From 74feefb2c7e1eca1673a929a599e0d484005189b Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 15 Aug 2025 11:29:53 +0200 Subject: [PATCH 01/44] Added a setting to control the diving parameters --- .../linear_programming/cuopt/run_mip.cpp | 41 ++++++++++++------- .../mip/solver_settings.hpp | 6 +++ cpp/src/dual_simplex/branch_and_bound.cpp | 5 ++- cpp/src/dual_simplex/branch_and_bound.hpp | 5 ++- .../dual_simplex/simplex_solver_settings.hpp | 2 + cpp/src/dual_simplex/solve.cpp | 9 +++- cpp/src/dual_simplex/solve.hpp | 3 ++ cpp/src/mip/solver.cu | 2 +- cpp/tests/dual_simplex/unit_tests/solve.cpp | 10 ++++- 9 files changed, 61 insertions(+), 22 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index b20ae26ea6..82763daa84 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -151,7 +151,8 @@ int run_single_file(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, - double time_limit) + double time_limit, + bool use_diving) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -168,11 +169,13 @@ int run_single_file(std::string file_path, } } + settings.search_strategy.use_diving = use_diving; + constexpr bool input_mps_strict = false; cuopt::mps_parser::mps_data_model_t mps_data_model; bool parsing_failed = false; { - CUOPT_LOG_INFO("running file %s on gpu : %d", base_filename.c_str(), device); + CUOPT_LOG_INFO("Running file %s", base_filename.c_str()); try { mps_data_model = cuopt::mps_parser::parse_mps(file_path, input_mps_strict); } catch (const std::logic_error& e) { @@ -232,14 +235,15 @@ int run_single_file(std::string file_path, } else { CUOPT_LOG_INFO("%s: no solution found", base_filename.c_str()); } - std::stringstream ss; - int decimal_places = 2; - ss << std::fixed << std::setprecision(decimal_places) << base_filename << "," << sol_found << "," - << obj_val << "," << benchmark_info.objective_of_initial_population << "," - << benchmark_info.last_improvement_of_best_feasible << "," - << benchmark_info.last_improvement_after_recombination << "\n"; - write_to_output_file(out_dir, base_filename, device, batch_id, ss.str()); - CUOPT_LOG_INFO("Results written to the file %s", base_filename.c_str()); + // std::stringstream ss; + // int decimal_places = 2; + // ss << std::fixed << std::setprecision(decimal_places) << base_filename << "," << sol_found << + // "," + // << obj_val << "," << benchmark_info.objective_of_initial_population << "," + // << benchmark_info.last_improvement_of_best_feasible << "," + // << benchmark_info.last_improvement_after_recombination << "\n"; + // write_to_output_file(out_dir, base_filename, device, batch_id, ss.str()); + // CUOPT_LOG_INFO("Results written to the file %s", base_filename.c_str()); return sol_found; } @@ -252,7 +256,8 @@ void run_single_file_mp(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, - double time_limit) + double time_limit, + bool use_diving) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -266,7 +271,8 @@ void run_single_file_mp(std::string file_path, num_cpu_threads, write_log_file, log_to_console, - time_limit); + time_limit, + use_diving); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes exit(sol_found); @@ -340,6 +346,10 @@ int main(int argc, char* argv[]) .scan<'g', double>() .default_value(std::numeric_limits::max()); + program.add_argument("--diving") + .help("use diving during branch and bound(t/f)") + .default_value(std::string("f")); + // Parse arguments try { program.parse_args(argc, argv); @@ -366,6 +376,7 @@ int main(int argc, char* argv[]) int num_cpu_threads = program.get("--num-cpu-threads"); bool write_log_file = program.get("--write-log-file")[0] == 't'; bool log_to_console = program.get("--log-to-console")[0] == 't'; + bool use_diving = program.get("--diving")[0] == 't'; if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); @@ -451,7 +462,8 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, - time_limit); + time_limit, + use_diving); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; exit(1); @@ -480,7 +492,8 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, - time_limit); + time_limit, + use_diving); } return 0; diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index fbb75d80b1..584de2d72a 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -35,6 +35,10 @@ struct benchmark_info_t { double objective_of_initial_population = std::numeric_limits::max(); }; +struct bnb_search_strategy_t { + bool use_diving = false; +}; + // Forward declare solver_settings_t for friend class template class solver_settings_t; @@ -79,6 +83,8 @@ class mip_solver_settings_t { f_t relative_mip_gap = 1.0e-4; }; + bnb_search_strategy_t search_strategy; + /** * @brief Get the tolerance settings as a single structure. */ diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9067040129..9dfc4d1079 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -399,8 +399,9 @@ bool branch_and_bound_t::repair_solution( template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, - const simplex_solver_settings_t& solver_settings) - : original_problem(user_problem), settings(solver_settings), original_lp(1, 1, 1) + const simplex_solver_settings_t& solver_settings, + const bnb_search_strategy_t strategy) + : original_problem(user_problem), settings(solver_settings), search_strategy(strategy), original_lp(1, 1, 1) { start_time = tic(); convert_user_problem(original_problem, settings, original_lp, new_slacks); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index da594bf93b..4e315f4f32 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -25,6 +25,7 @@ #include #include +#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex { @@ -45,7 +46,8 @@ template class branch_and_bound_t { public: branch_and_bound_t(const user_problem_t& user_problem, - const simplex_solver_settings_t& solver_settings); + const simplex_solver_settings_t& solver_settings, + const bnb_search_strategy_t strategy); // Set an initial guess based on the user_problem. This should be called before solve. void set_initial_guess(const std::vector& user_guess) { guess = user_guess; } @@ -65,6 +67,7 @@ class branch_and_bound_t { private: const user_problem_t& original_problem; const simplex_solver_settings_t settings; + bnb_search_strategy_t search_strategy; f_t start_time; std::vector guess; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index a51ed19bcf..ead68022fc 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -113,6 +113,8 @@ struct simplex_solver_settings_t { mutable logger_t log; std::atomic* concurrent_halt; // if nullptr ignored, if !nullptr, 0 if solver should // continue, 1 if solver should halt + + }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index e665bae973..0747d947ef 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -35,6 +35,7 @@ #include #include #include +#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex { @@ -265,11 +266,12 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, template i_t solve(const user_problem_t& problem, const simplex_solver_settings_t& settings, + const bnb_search_strategy_t search_strategy, std::vector& primal_solution) { i_t status; if (is_mip(problem) && !settings.relaxation) { - branch_and_bound_t branch_and_bound(problem, settings); + branch_and_bound_t branch_and_bound(problem, settings, search_strategy); mip_solution_t mip_solution(problem.num_cols); mip_status_t mip_status = branch_and_bound.solve(mip_solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -302,12 +304,13 @@ i_t solve(const user_problem_t& problem, template i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, + const bnb_search_strategy_t search_strategy, const std::vector& guess, mip_solution_t& solution) { i_t status; if (is_mip(problem)) { - branch_and_bound_t branch_and_bound(problem, settings); + branch_and_bound_t branch_and_bound(problem, settings, search_strategy); branch_and_bound.set_initial_guess(guess); mip_status_t mip_status = branch_and_bound.solve(solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -348,11 +351,13 @@ template lp_status_t solve_linear_program(const user_problem_t& use template int solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, + const bnb_search_strategy_t search_strategy, std::vector& primal_solution); template int solve_mip_with_guess( const user_problem_t& problem, const simplex_solver_settings_t& settings, + const bnb_search_strategy_t search_strategy, const std::vector& guess, mip_solution_t& solution); diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index f20844d6e6..46303a8d62 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -21,6 +21,7 @@ #include #include #include +#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex { @@ -67,12 +68,14 @@ i_t solve_mip(const user_problem_t& user_problem, mip_solution_t i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, + const bnb_search_strategy_t search_strategy, const std::vector& guess, mip_solution_t& solution); template i_t solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, + const bnb_search_strategy_t search_strategy, std::vector& primal_solution); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 0f2117991f..72ccbd6195 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -190,7 +190,7 @@ solution_t mip_solver_t::run_solver() // Create the branch and bound object branch_and_bound = std::make_unique>( - branch_and_bound_problem, branch_and_bound_settings); + branch_and_bound_problem, branch_and_bound_settings, context.settings.search_strategy); // Set the primal heuristics -> branch and bound callback context.problem_ptr->branch_and_bound_callback = diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 0743c08d78..56d04f04b8 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -27,6 +27,7 @@ #include #include +#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex::test { @@ -87,6 +88,8 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::CONTINUOUS; user_problem.var_types[1] = dual_simplex::variable_type_t::CONTINUOUS; + bnb_search_strategy_t search_strategy; + double start_time = dual_simplex::tic(); dual_simplex::simplex_solver_settings_t settings; dual_simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); @@ -100,7 +103,7 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::INTEGER; user_problem.var_types[1] = dual_simplex::variable_type_t::INTEGER; - EXPECT_EQ((dual_simplex::solve(user_problem, settings, solution.x)), 0); + EXPECT_EQ((dual_simplex::solve(user_problem, settings, search_strategy, solution.x)), 0); } TEST(dual_simplex, burglar) @@ -160,9 +163,12 @@ TEST(dual_simplex, burglar) for (int j = 0; j < num_items; ++j) { user_problem.var_types[j] = cuopt::linear_programming::dual_simplex::variable_type_t::INTEGER; } + + bnb_search_strategy_t search_strategy; + cuopt::linear_programming::dual_simplex::simplex_solver_settings_t settings; std::vector solution(num_items); - EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve(user_problem, settings, solution)), 0); + EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve(user_problem, settings, search_strategy, solution)), 0); double objective = 0.0; for (int j = 0; j < num_items; ++j) { objective += value[j] * solution[j]; From 326eec69561a0b3bfb4c6fb5e92986d8a4f76450 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 15 Aug 2025 12:25:44 +0200 Subject: [PATCH 02/44] Changed branch and bound to use diving. --- .../linear_programming/cuopt/run_mip.cpp | 3 +- cpp/src/dual_simplex/branch_and_bound.cpp | 480 +++++++++++++++++- cpp/src/dual_simplex/branch_and_bound.hpp | 3 + 3 files changed, 483 insertions(+), 3 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 82763daa84..3c0ce2f56e 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -17,6 +17,7 @@ #include "initial_solution_reader.hpp" #include "mip_test_instances.hpp" +#include #include #include #include @@ -377,7 +378,7 @@ int main(int argc, char* argv[]) bool write_log_file = program.get("--write-log-file")[0] == 't'; bool log_to_console = program.get("--log-to-console")[0] == 't'; bool use_diving = program.get("--diving")[0] == 't'; - + if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); result_file = out_dir + "/final_result.csv"; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9dfc4d1079..b33388b060 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -15,6 +15,7 @@ * limitations under the License. */ +#include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include namespace cuopt::linear_programming::dual_simplex { @@ -401,7 +403,10 @@ branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, const bnb_search_strategy_t strategy) - : original_problem(user_problem), settings(solver_settings), search_strategy(strategy), original_lp(1, 1, 1) + : original_problem(user_problem), + settings(solver_settings), + search_strategy(strategy), + original_lp(1, 1, 1) { start_time = tic(); convert_user_problem(original_problem, settings, original_lp, new_slacks); @@ -421,7 +426,466 @@ branch_and_bound_t::branch_and_bound_t( } template -mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) +mip_status_t branch_and_bound_t::solve_diving(mip_solution_t& solution) +{ + mip_status_t status = mip_status_t::UNSET; + mip_solution_t incumbent(original_lp.num_cols); + + // Handle initial guess + if (guess.size() != 0) { + std::vector crushed_guess; + crush_primal_solution(original_problem, original_lp, guess, new_slacks, crushed_guess); + f_t primal_err; + f_t bound_err; + i_t num_fractional; + const bool feasible = check_guess( + original_lp, settings, var_types, crushed_guess, primal_err, bound_err, num_fractional); + if (feasible) { + const f_t computed_obj = compute_objective(original_lp, crushed_guess); + global_variables::mutex_upper.lock(); + incumbent.set_incumbent_solution(computed_obj, crushed_guess); + global_variables::upper_bound = computed_obj; + global_variables::mutex_upper.unlock(); + } + } + + // Calculate the LP relaxation of the root node + lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); + std::vector root_vstatus; + std::vector edge_norms; + settings.log.printf("Solving LP root relaxation\n"); + simplex_solver_settings_t lp_settings = settings; + lp_settings.inside_mip = 1; + lp_status_t root_status = solve_linear_program_advanced( + original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); + f_t total_lp_solve_time = toc(start_time); + assert(root_vstatus.size() == original_lp.num_cols); + if (root_status == lp_status_t::INFEASIBLE) { + settings.log.printf("MIP Infeasible\n"); + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::INFEASIBLE; + } + if (root_status == lp_status_t::UNBOUNDED) { + settings.log.printf("MIP Unbounded\n"); + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::UNBOUNDED; + } + if (root_status == lp_status_t::TIME_LIMIT) { + settings.log.printf("Hit time limit\n"); + return mip_status_t::TIME_LIMIT; + } + set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); + + std::vector fractional; + const i_t num_fractional = + fractional_variables(settings, root_relax_soln.x, var_types, fractional); + const f_t root_objective = compute_objective(original_lp, root_relax_soln.x); + if (settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); + settings.set_simplex_solution_callback(original_x, + compute_user_objective(original_lp, root_objective)); + } + global_variables::mutex_lower.lock(); + f_t lower_bound = global_variables::lower_bound = root_objective; + global_variables::mutex_lower.unlock(); + + // Check if the MIP has already been solved at the root node + if (num_fractional == 0) { + global_variables::mutex_upper.lock(); + incumbent.set_incumbent_solution(root_objective, root_relax_soln.x); + global_variables::upper_bound = root_objective; + global_variables::mutex_upper.unlock(); + // We should be done here + uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); + solution.objective = incumbent.objective; + solution.lower_bound = lower_bound; + solution.nodes_explored = 0; + solution.simplex_iterations = root_relax_soln.iterations; + settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", + compute_user_objective(original_lp, root_objective), + toc(start_time)); + if (settings.solution_callback != nullptr) { + settings.solution_callback(solution.x, solution.objective); + } + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::OPTIMAL; + } + + // Apply strong branching as the initial pseudocost for the fractional variables + pseudo_costs_t pc(original_lp.num_cols); + strong_branching(original_lp, + settings, + start_time, + var_types, + root_relax_soln.x, + fractional, + root_objective, + root_vstatus, + edge_norms, + pc); + + std::vector*> node_stack; + + i_t num_nodes = 0; + mip_node_t root_node(root_objective, root_vstatus); + graphviz_node(settings, &root_node, "lower bound", lower_bound); + + // Choose variable to branch on + logger_t log; + log.log = false; + const i_t branch_var = + pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); + + // down child + std::unique_ptr> down_child = + std::make_unique>(original_lp, + &root_node, + ++num_nodes, + branch_var, + 0, + root_relax_soln.x[branch_var], + root_vstatus); + + graphviz_edge(settings, + &root_node, + down_child.get(), + branch_var, + 0, + std::floor(root_relax_soln.x[branch_var])); + + // up child + std::unique_ptr> up_child = + std::make_unique>(original_lp, + &root_node, + ++num_nodes, + branch_var, + 1, + root_relax_soln.x[branch_var], + root_vstatus); + + graphviz_edge( + settings, &root_node, up_child.get(), branch_var, 0, std::ceil(root_relax_soln.x[branch_var])); + + assert(root_vstatus.size() == original_lp.num_cols); + node_stack.push_back(down_child.get()); // the stack does not own the unique_ptr the tree does + node_stack.push_back(up_child.get()); // the stack does not own the unqiue_ptr the tree does + root_node.add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree + lp_problem_t leaf_problem = + original_lp; // Make a copy of the original LP. We will modify its bounds at each leaf + f_t gap = get_upper_bound() - lower_bound; + i_t nodes_explored = 0; + settings.log.printf( + "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " + " Time \n"); + global_variables::mutex_branching.lock(); + global_variables::currently_branching = true; + global_variables::mutex_branching.unlock(); + + f_t total_lp_iters = 0.0; + f_t last_log = 0; + while (gap > settings.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && + node_stack.size() > 0) { + // Check if there are any solutions to repair + std::vector> to_repair; + global_variables::mutex_repair.lock(); + if (global_variables::repair_queue.size() > 0) { + to_repair = global_variables::repair_queue; + global_variables::repair_queue.clear(); + } + global_variables::mutex_repair.unlock(); + + if (to_repair.size() > 0) { + settings.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); + for (const std::vector& potential_solution : to_repair) { + std::vector repaired_solution; + f_t repaired_obj; + bool is_feasible = repair_solution( + root_vstatus, edge_norms, potential_solution, repaired_obj, repaired_solution); + if (is_feasible) { + global_variables::mutex_upper.lock(); + if (repaired_obj < global_variables::upper_bound) { + global_variables::upper_bound = repaired_obj; + incumbent.set_incumbent_solution(repaired_obj, repaired_solution); + + settings.log.printf( + "H %+13.6e %+10.6e %s %9.2f\n", + compute_user_objective(original_lp, repaired_obj), + compute_user_objective(original_lp, lower_bound), + user_mip_gap(compute_user_objective(original_lp, repaired_obj), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + toc(start_time)); + if (settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, repaired_solution, original_x); + settings.solution_callback(original_x, repaired_obj); + } + } + global_variables::mutex_upper.unlock(); + } + } + } + + // Get a node off the stack + mip_node_t* node_ptr = node_stack.back(); + node_stack.pop_back(); // Remove node from the stack + f_t upper_bound = get_upper_bound(); + + gap = upper_bound - lower_bound; + const i_t leaf_depth = node_ptr->depth; + f_t now = toc(start_time); + f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || + nodes_explored < 1000) && + (time_since_log >= 1) || + (time_since_log > 60) || now > settings.time_limit) { + settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + node_stack.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound), + leaf_depth, + nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + now); + last_log = tic(); + } + + if (now > settings.time_limit) { + settings.log.printf("Hit time limit. Stoppping\n"); + status = mip_status_t::TIME_LIMIT; + break; + } + + // Set the correct bounds for the leaf problem + leaf_problem.lower = original_lp.lower; + leaf_problem.upper = original_lp.upper; + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); + + std::vector& leaf_vstatus = node_ptr->vstatus; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + + i_t node_iter = 0; + assert(leaf_vstatus.size() == leaf_problem.num_cols); + f_t lp_start_time = tic(); + std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; + + // Solve the LP relaxation of the current node using Dual Simplex and the previous basis + simplex_solver_settings_t lp_settings = settings; + lp_settings.set_log(false); + lp_settings.cut_off = upper_bound + settings.dual_tol; + lp_settings.inside_mip = 2; + dual::status_t lp_status = dual_phase2(2, + 0, + lp_start_time, + leaf_problem, + lp_settings, + leaf_vstatus, + leaf_solution, + node_iter, + leaf_edge_norms); + if (lp_status == dual::status_t::NUMERICAL) { + settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", nodes_explored); + lp_status_t second_status = solve_linear_program_advanced( + leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); + lp_status = convert_lp_status_to_dual_status(second_status); + } + total_lp_solve_time += toc(lp_start_time); + total_lp_iters += node_iter; + + nodes_explored++; + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { + node_ptr->lower_bound = inf; + std::vector*> stack; + node_ptr->set_status(node_status_t::INFEASIBLE, stack); + graphviz_node(settings, node_ptr, "infeasible", 0.0); + remove_fathomed_nodes(stack); + // Node was infeasible. Do not branch + } else if (lp_status == dual::status_t::CUTOFF) { + node_ptr->lower_bound = upper_bound; + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "cut off", leaf_objective); + remove_fathomed_nodes(stack); + // Node was cut off. Do not branch + + } else if (lp_status == dual::status_t::OPTIMAL) { + // LP was feasible + std::vector fractional; + const i_t leaf_fractional = + fractional_variables(settings, leaf_solution.x, var_types, fractional); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + + pc.update_pseudo_costs(node_ptr, leaf_objective); + node_ptr->lower_bound = leaf_objective; + + constexpr f_t fathom_tol = 1e-5; + if (leaf_fractional == 0) { + bool send_solution = false; + global_variables::mutex_upper.lock(); + if (leaf_objective < global_variables::upper_bound) { + incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); + global_variables::upper_bound = upper_bound = leaf_objective; + gap = upper_bound - lower_bound; + settings.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + node_stack.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound), + leaf_depth, + nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + toc(start_time)); + send_solution = true; + } + global_variables::mutex_upper.unlock(); + if (send_solution && settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); + settings.solution_callback(original_x, upper_bound); + } + graphviz_node(settings, node_ptr, "integer feasible", leaf_objective); + std::vector*> stack; + node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); + remove_fathomed_nodes(stack); + + } else if (leaf_objective <= upper_bound + fathom_tol) { + // Choose fractional variable to branch on + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + assert(leaf_vstatus.size() == leaf_problem.num_cols); + + // down child + std::unique_ptr> down_child = + std::make_unique>(original_lp, + node_ptr, + ++num_nodes, + branch_var, + 0, + leaf_solution.x[branch_var], + leaf_vstatus); + graphviz_edge(settings, + node_ptr, + down_child.get(), + branch_var, + 0, + std::floor(leaf_solution.x[branch_var])); + // up child + std::unique_ptr> up_child = + std::make_unique>(original_lp, + node_ptr, + ++num_nodes, + branch_var, + 1, + leaf_solution.x[branch_var], + leaf_vstatus); + graphviz_edge(settings, + node_ptr, + up_child.get(), + branch_var, + 0, + std::ceil(leaf_solution.x[branch_var])); + + // Martin's child selection + const f_t down_val = std::floor(root_relax_soln.x[branch_var]); + const f_t up_val = std::ceil(root_relax_soln.x[branch_var]); + const f_t down_dist = leaf_solution.x[branch_var] - down_val; + const f_t up_dist = up_val - leaf_solution.x[branch_var]; + + if (down_dist < up_dist) { + node_stack.push_back(up_child.get()); + node_stack.push_back(down_child.get()); + } else { + node_stack.push_back(up_child.get()); + node_stack.push_back(down_child.get()); + } + + node_ptr->add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree + } else { + graphviz_node(settings, node_ptr, "fathomed", leaf_objective); + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + remove_fathomed_nodes(stack); + } + } else { + graphviz_node(settings, node_ptr, "numerical", 0.0); + settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", + lp_status); + status = mip_status_t::NUMERICAL; + break; + } + } + global_variables::mutex_branching.lock(); + global_variables::currently_branching = false; + global_variables::mutex_branching.unlock(); + + if (node_stack.size() == 0) { + global_variables::mutex_lower.lock(); + lower_bound = global_variables::lower_bound = root_node.lower_bound; + global_variables::mutex_lower.unlock(); + gap = get_upper_bound() - lower_bound; + } + + settings.log.printf( + "Explored %d nodes in %.2fs.\nAbsolute Gap %e Objective %.16e Lower Bound %.16e\n", + nodes_explored, + toc(start_time), + gap, + compute_user_objective(original_lp, get_upper_bound()), + compute_user_objective(original_lp, lower_bound)); + + if (gap <= settings.absolute_mip_gap_tol || + relative_gap(get_upper_bound(), lower_bound) <= settings.relative_mip_gap_tol) { + status = mip_status_t::OPTIMAL; + if (gap > 0 && gap <= settings.absolute_mip_gap_tol) { + settings.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", + settings.absolute_mip_gap_tol); + } else if (gap > 0 && + relative_gap(get_upper_bound(), lower_bound) <= settings.relative_mip_gap_tol) { + settings.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", + settings.relative_mip_gap_tol); + } else { + settings.log.printf("Optimal solution found.\n"); + } + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + } + + if (node_stack.size() == 0 && get_upper_bound() == inf) { + settings.log.printf("Integer infeasible.\n"); + status = mip_status_t::INFEASIBLE; + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + } + + uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); + solution.objective = incumbent.objective; + solution.lower_bound = lower_bound; + solution.nodes_explored = nodes_explored; + solution.simplex_iterations = total_lp_iters; + return status; +} + +template +mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; mip_solution_t incumbent(original_lp.num_cols); @@ -872,6 +1336,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return status; } +template +mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) +{ + if (search_strategy.use_diving) { + settings.log.printf("Using DFS as search strategy.\n"); + return solve_diving(solution); + } else { + settings.log.printf("Using BFS as search strategy.\n"); + return solve_bfs(solution); + } +} + #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template class branch_and_bound_t; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 4e315f4f32..2d46e149ab 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -75,6 +75,9 @@ class branch_and_bound_t { lp_problem_t original_lp; std::vector new_slacks; std::vector var_types; + + mip_status_t solve_bfs(mip_solution_t& solution); + mip_status_t solve_diving(mip_solution_t& solution); }; } // namespace cuopt::linear_programming::dual_simplex From e6db94e8a208c218e529002bf4720ff75613d3dd Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 18 Aug 2025 14:31:24 +0200 Subject: [PATCH 03/44] Expanded options for the search strategy --- .../linear_programming/cuopt/run_mip.cpp | 31 +++++++++++++------ .../mip/solver_settings.hpp | 8 ++++- cpp/src/dual_simplex/branch_and_bound.cpp | 27 +++++++++++----- cpp/src/dual_simplex/branch_and_bound.hpp | 3 +- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 3c0ce2f56e..a894592677 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -153,7 +153,7 @@ int run_single_file(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - bool use_diving) + int diving) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -170,7 +170,7 @@ int run_single_file(std::string file_path, } } - settings.search_strategy.use_diving = use_diving; + settings.search_strategy.strategy = diving; constexpr bool input_mps_strict = false; cuopt::mps_parser::mps_data_model_t mps_data_model; @@ -258,7 +258,7 @@ void run_single_file_mp(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - bool use_diving) + int diving) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -273,7 +273,7 @@ void run_single_file_mp(std::string file_path, write_log_file, log_to_console, time_limit, - use_diving); + diving); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes exit(sol_found); @@ -348,7 +348,7 @@ int main(int argc, char* argv[]) .default_value(std::numeric_limits::max()); program.add_argument("--diving") - .help("use diving during branch and bound(t/f)") + .help("diving strategy (bfs, dfs or bfs_diving)") .default_value(std::string("f")); // Parse arguments @@ -377,8 +377,21 @@ int main(int argc, char* argv[]) int num_cpu_threads = program.get("--num-cpu-threads"); bool write_log_file = program.get("--write-log-file")[0] == 't'; bool log_to_console = program.get("--log-to-console")[0] == 't'; - bool use_diving = program.get("--diving")[0] == 't'; - + int diving; + + std::string diving_cli = program.get("--diving"); + + if (diving_cli == "bfs") { + diving = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST; + } else if (diving_cli == "dfs") { + diving = cuopt::linear_programming::bnb_search_strategy_t::DEPTH_FIRST; + } else if (diving_cli == "bfs_diving") { + diving = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST_WITH_DIVING; + } else { + std::cout << "Incorrect diving strategy!\n"; + exit(1); + } + if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); result_file = out_dir + "/final_result.csv"; @@ -464,7 +477,7 @@ int main(int argc, char* argv[]) write_log_file, log_to_console, time_limit, - use_diving); + diving); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; exit(1); @@ -494,7 +507,7 @@ int main(int argc, char* argv[]) write_log_file, log_to_console, time_limit, - use_diving); + diving); } return 0; diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 584de2d72a..6c99b2a8a4 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -36,7 +36,13 @@ struct benchmark_info_t { }; struct bnb_search_strategy_t { - bool use_diving = false; + enum { + BEST_FIRST = 0, + DEPTH_FIRST = 1, + BEST_FIRST_WITH_DIVING = 2 + }; + + int strategy = BEST_FIRST; }; // Forward declare solver_settings_t for friend class diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b33388b060..2fb6be15da 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -36,6 +36,7 @@ #include #include #include +#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex { @@ -426,7 +427,7 @@ branch_and_bound_t::branch_and_bound_t( } template -mip_status_t branch_and_bound_t::solve_diving(mip_solution_t& solution) +mip_status_t branch_and_bound_t::solve_dfs(mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; mip_solution_t incumbent(original_lp.num_cols); @@ -1339,13 +1340,25 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { - if (search_strategy.use_diving) { - settings.log.printf("Using DFS as search strategy.\n"); - return solve_diving(solution); - } else { - settings.log.printf("Using BFS as search strategy.\n"); - return solve_bfs(solution); + switch (search_strategy.strategy) { + case bnb_search_strategy_t::BEST_FIRST: + settings.log.printf("Using BFS as search strategy.\n"); + return solve_bfs(solution); + + case bnb_search_strategy_t::DEPTH_FIRST: + settings.log.printf("Using DFS as search strategy.\n"); + return solve_dfs(solution); + + case bnb_search_strategy_t::BEST_FIRST_WITH_DIVING: + settings.log.printf("Using BFS with diving as search strategy.\n"); + return solve_bfs(solution); + + default: + settings.log.printf("Invalid search strategy! Exiting..."); + exit(EXIT_FAILURE); + break; } + } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 2d46e149ab..12a3296b60 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -77,7 +77,8 @@ class branch_and_bound_t { std::vector var_types; mip_status_t solve_bfs(mip_solution_t& solution); - mip_status_t solve_diving(mip_solution_t& solution); + mip_status_t solve_bfs_diving(mip_solution_t& solution); + mip_status_t solve_dfs(mip_solution_t& solution); }; } // namespace cuopt::linear_programming::dual_simplex From 71955f4c83a3b237faa72f0f339efa23f55cfbc4 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 19 Aug 2025 12:04:51 +0200 Subject: [PATCH 04/44] Integrated diving with best-first search. --- cpp/src/dual_simplex/branch_and_bound.cpp | 298 +++++++++++++++++++--- cpp/src/dual_simplex/branch_and_bound.hpp | 9 + cpp/src/dual_simplex/mip_node.hpp | 5 +- 3 files changed, 280 insertions(+), 32 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 2fb6be15da..8f630130d1 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -31,6 +31,8 @@ #include #include +#include +#include #include #include #include @@ -532,12 +534,12 @@ mip_status_t branch_and_bound_t::solve_dfs(mip_solution_t& s edge_norms, pc); - std::vector*> node_stack; - i_t num_nodes = 0; mip_node_t root_node(root_objective, root_vstatus); graphviz_node(settings, &root_node, "lower bound", lower_bound); + std::vector*> node_stack; + // Choose variable to branch on logger_t log; log.log = false; @@ -999,46 +1001,40 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s // Choose variable to branch on logger_t log; log.log = false; - const i_t branch_var = + i_t branch_var = pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); + f_t branch_var_val = root_relax_soln.x[branch_var]; + + std::future diving_thread; + + if (search_strategy.strategy == bnb_search_strategy_t::BEST_FIRST_WITH_DIVING) { + diving_thread = std::async(std::launch::async, [&]() { + return diving( + root_objective, edge_norms, pc, branch_var, branch_var_val, root_vstatus, incumbent); + }); + } // down child - std::unique_ptr> down_child = - std::make_unique>(original_lp, - &root_node, - ++num_nodes, - branch_var, - 0, - root_relax_soln.x[branch_var], - root_vstatus); + std::unique_ptr> down_child = std::make_unique>( + original_lp, &root_node, ++num_nodes, branch_var, 0, branch_var_val, root_vstatus); - graphviz_edge(settings, - &root_node, - down_child.get(), - branch_var, - 0, - std::floor(root_relax_soln.x[branch_var])); + graphviz_edge(settings, &root_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); // up child - std::unique_ptr> up_child = - std::make_unique>(original_lp, - &root_node, - ++num_nodes, - branch_var, - 1, - root_relax_soln.x[branch_var], - root_vstatus); + std::unique_ptr> up_child = std::make_unique>( + original_lp, &root_node, ++num_nodes, branch_var, 1, branch_var_val, root_vstatus); - graphviz_edge( - settings, &root_node, up_child.get(), branch_var, 0, std::ceil(root_relax_soln.x[branch_var])); + graphviz_edge(settings, &root_node, up_child.get(), branch_var, 0, std::ceil(branch_var_val)); assert(root_vstatus.size() == original_lp.num_cols); heap.push(down_child.get()); // the heap does not own the unique_ptr the tree does heap.push(up_child.get()); // the heap does not own the unqiue_ptr the tree does root_node.add_children(std::move(down_child), std::move(up_child)); // child pointers moved into the tree - lp_problem_t leaf_problem = - original_lp; // Make a copy of the original LP. We will modify its bounds at each leaf + + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp; + f_t gap = get_upper_bound() - lower_bound; i_t nodes_explored = 0; settings.log.printf( @@ -1284,6 +1280,9 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s break; } } + + mip_status_t diving_status = diving_thread.get(); + global_variables::mutex_branching.lock(); global_variables::currently_branching = false; global_variables::mutex_branching.unlock(); @@ -1337,6 +1336,246 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s return status; } +template +mip_status_t branch_and_bound_t::diving(f_t root_objective, + std::vector& edge_norms, + pseudo_costs_t pc, + i_t branch_var, + f_t branch_var_val, + const std::vector& root_vstatus, + mip_solution_t& incumbent) +{ + logger_t log; + log.log = false; + + mip_node_t root_node(root_objective, root_vstatus); + + mip_status_t status = mip_status_t::UNSET; + f_t lower_bound = root_objective; + std::vector*> node_stack; + i_t num_nodes = 1; + + // down child + std::unique_ptr> down_child = + std::make_unique>(original_lp, + &root_node, + ++num_nodes, + branch_var, + 0, + branch_var_val, + root_vstatus); + + // up child + std::unique_ptr> up_child = + std::make_unique>(original_lp, + &root_node, + ++num_nodes, + branch_var, + 1, + branch_var_val, + root_vstatus); + + node_stack.push_back(down_child.get()); // the stack does not own the unique_ptr the tree does + node_stack.push_back(up_child.get()); // the stack does not own the unqiue_ptr the tree does + root_node.add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree + + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp; + f_t gap = get_upper_bound() - lower_bound; + i_t nodes_explored = 0; + f_t total_lp_iters = 0.0; + f_t last_log = 0; + while (gap > settings.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && + node_stack.size() > 0) { + // Get a node off the stack + mip_node_t* node_ptr = node_stack.back(); + node_stack.pop_back(); // Remove node from the stack + f_t upper_bound = get_upper_bound(); + + gap = upper_bound - lower_bound; + const i_t leaf_depth = node_ptr->depth; + f_t now = toc(start_time); + f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + + // if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || + // nodes_explored < 1000) && + // (time_since_log >= 1) || + // (time_since_log > 60) || now > settings.time_limit) { + // settings.log.printf("D%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + // nodes_explored, + // node_stack.size(), + // compute_user_objective(original_lp, upper_bound), + // compute_user_objective(original_lp, lower_bound), + // leaf_depth, + // nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, + // user_mip_gap(compute_user_objective(original_lp, upper_bound), + // compute_user_objective(original_lp, lower_bound)) + // .c_str(), + // now); + // last_log = tic(); + // } + + if (now > settings.time_limit) { + status = mip_status_t::TIME_LIMIT; + break; + } + + // Set the correct bounds for the leaf problem + leaf_problem.lower = original_lp.lower; + leaf_problem.upper = original_lp.upper; + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); + + std::vector& leaf_vstatus = node_ptr->vstatus; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + + i_t node_iter = 0; + assert(leaf_vstatus.size() == leaf_problem.num_cols); + f_t lp_start_time = tic(); + std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; + + // Solve the LP relaxation of the current node using Dual Simplex and the previous basis + simplex_solver_settings_t lp_settings = settings; + lp_settings.set_log(false); + lp_settings.cut_off = upper_bound + settings.dual_tol; + lp_settings.inside_mip = 2; + dual::status_t lp_status = dual_phase2(2, + 0, + lp_start_time, + leaf_problem, + lp_settings, + leaf_vstatus, + leaf_solution, + node_iter, + leaf_edge_norms); + if (lp_status == dual::status_t::NUMERICAL) { + settings.log.printf("Numerical issue node %d (diving). Resolving from scratch.\n", + nodes_explored); + lp_status_t second_status = solve_linear_program_advanced( + leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); + lp_status = convert_lp_status_to_dual_status(second_status); + } + total_lp_iters += node_iter; + + nodes_explored++; + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { + node_ptr->lower_bound = inf; + std::vector*> stack; + node_ptr->set_status(node_status_t::INFEASIBLE, stack); + graphviz_node(settings, node_ptr, "infeasible", 0.0); + remove_fathomed_nodes(stack); + // Node was infeasible. Do not branch + + } else if (lp_status == dual::status_t::CUTOFF) { + node_ptr->lower_bound = upper_bound; + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "cut off", leaf_objective); + remove_fathomed_nodes(stack); + // Node was cut off. Do not branch + + } else if (lp_status == dual::status_t::OPTIMAL) { + // LP was feasible + std::vector fractional; + const i_t leaf_fractional = + fractional_variables(settings, leaf_solution.x, var_types, fractional); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + + pc.update_pseudo_costs(node_ptr, leaf_objective); + node_ptr->lower_bound = leaf_objective; + + constexpr f_t fathom_tol = 1e-5; + if (leaf_fractional == 0) { + bool send_solution = false; + global_variables::mutex_upper.lock(); + if (leaf_objective < global_variables::upper_bound) { + incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); + global_variables::upper_bound = upper_bound = leaf_objective; + gap = upper_bound - lower_bound; + settings.log.printf("D%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + node_stack.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound), + leaf_depth, + nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + toc(start_time)); + send_solution = true; + } + + if (send_solution && settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); + settings.solution_callback(original_x, upper_bound); + } + global_variables::mutex_upper.unlock(); + + std::vector*> stack; + node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); + remove_fathomed_nodes(stack); + + } else if (leaf_objective <= upper_bound + fathom_tol) { + // Choose fractional variable to branch on + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + assert(leaf_vstatus.size() == leaf_problem.num_cols); + + // down child + std::unique_ptr> down_child = + std::make_unique>(original_lp, + node_ptr, + ++num_nodes, + branch_var, + 0, + leaf_solution.x[branch_var], + leaf_vstatus); + + // up child + std::unique_ptr> up_child = + std::make_unique>(original_lp, + node_ptr, + ++num_nodes, + branch_var, + 1, + leaf_solution.x[branch_var], + leaf_vstatus); + + // Martin's child selection + const f_t down_val = std::floor(root_node.fractional_val); + const f_t up_val = std::ceil(root_node.fractional_val); + const f_t down_dist = leaf_solution.x[branch_var] - down_val; + const f_t up_dist = up_val - leaf_solution.x[branch_var]; + + if (down_dist < up_dist) { + node_stack.push_back(up_child.get()); + node_stack.push_back(down_child.get()); + } else { + node_stack.push_back(up_child.get()); + node_stack.push_back(down_child.get()); + } + + node_ptr->add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree + } else { + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + remove_fathomed_nodes(stack); + } + } else { + status = mip_status_t::NUMERICAL; + break; + } + } + + return status; +} + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -1358,7 +1597,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exit(EXIT_FAILURE); break; } - } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 12a3296b60..d6076780a0 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -79,6 +80,14 @@ class branch_and_bound_t { mip_status_t solve_bfs(mip_solution_t& solution); mip_status_t solve_bfs_diving(mip_solution_t& solution); mip_status_t solve_dfs(mip_solution_t& solution); + + mip_status_t diving(f_t root_objective, + std::vector& edge_norms, + pseudo_costs_t pc, + i_t branch_var, + f_t branch_var_val, + const std::vector& root_vstatus, + mip_solution_t& incumbent); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index cae93a4dc7..475a9efc6c 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -40,7 +40,7 @@ bool inactive_status(node_status_t status); template class mip_node_t { public: - mip_node_t(f_t root_lower_bound, std::vector& basis) + mip_node_t(f_t root_lower_bound, const std::vector& basis) : status(node_status_t::ACTIVE), lower_bound(root_lower_bound), depth(0), @@ -53,13 +53,14 @@ class mip_node_t { children[0] = nullptr; children[1] = nullptr; } + mip_node_t(const lp_problem_t& problem, mip_node_t* parent_node, i_t node_num, i_t branch_variable, i_t branch_direction, f_t branch_var_value, - std::vector& basis) + const std::vector& basis) : status(node_status_t::ACTIVE), lower_bound(parent_node->lower_bound), depth(parent_node->depth + 1), From b68295c1974ebfc6aa15c125cadc6d400212f77c Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 19 Aug 2025 13:44:22 +0200 Subject: [PATCH 05/44] Fixed invalid future in plain best-first. --- cpp/src/dual_simplex/branch_and_bound.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 8f630130d1..9e3612002f 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1281,7 +1281,9 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s } } - mip_status_t diving_status = diving_thread.get(); + if (search_strategy.strategy == bnb_search_strategy_t::BEST_FIRST_WITH_DIVING) { + mip_status_t diving_status = diving_thread.get(); + } global_variables::mutex_branching.lock(); global_variables::currently_branching = false; @@ -1375,6 +1377,7 @@ mip_status_t branch_and_bound_t::diving(f_t root_objective, branch_var_val, root_vstatus); + node_stack.push_back(down_child.get()); // the stack does not own the unique_ptr the tree does node_stack.push_back(up_child.get()); // the stack does not own the unqiue_ptr the tree does root_node.add_children(std::move(down_child), From 51524583127e4a786c3186f19b4c5a582e3116b7 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 20 Aug 2025 20:58:55 +0200 Subject: [PATCH 06/44] Clean the code and removed depth-first search --- .../linear_programming/cuopt/run_mip.cpp | 33 +- .../mip/solver_settings.hpp | 12 +- cpp/src/dual_simplex/branch_and_bound.cpp | 529 +----------------- cpp/src/dual_simplex/branch_and_bound.hpp | 8 +- cpp/src/dual_simplex/solve.cpp | 12 +- cpp/src/dual_simplex/solve.hpp | 4 +- cpp/src/mip/solver.cu | 4 +- cpp/tests/dual_simplex/unit_tests/solve.cpp | 10 +- 8 files changed, 54 insertions(+), 558 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index a894592677..ead62c5168 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -153,7 +153,7 @@ int run_single_file(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - int diving) + bool diving) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -170,7 +170,8 @@ int run_single_file(std::string file_path, } } - settings.search_strategy.strategy = diving; + settings.diving_settings.use_diving = diving; + constexpr bool input_mps_strict = false; cuopt::mps_parser::mps_data_model_t mps_data_model; @@ -258,7 +259,7 @@ void run_single_file_mp(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - int diving) + bool diving) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -348,9 +349,14 @@ int main(int argc, char* argv[]) .default_value(std::numeric_limits::max()); program.add_argument("--diving") - .help("diving strategy (bfs, dfs or bfs_diving)") + .help("enable diving (t/f)") .default_value(std::string("f")); + program.add_argument("--gpu") + .help("id of the GPU to use (default: 0)") + .scan<'i', int>() + .default_value(0); + // Parse arguments try { program.parse_args(argc, argv); @@ -377,20 +383,8 @@ int main(int argc, char* argv[]) int num_cpu_threads = program.get("--num-cpu-threads"); bool write_log_file = program.get("--write-log-file")[0] == 't'; bool log_to_console = program.get("--log-to-console")[0] == 't'; - int diving; - - std::string diving_cli = program.get("--diving"); - - if (diving_cli == "bfs") { - diving = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST; - } else if (diving_cli == "dfs") { - diving = cuopt::linear_programming::bnb_search_strategy_t::DEPTH_FIRST; - } else if (diving_cli == "bfs_diving") { - diving = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST_WITH_DIVING; - } else { - std::cout << "Incorrect diving strategy!\n"; - exit(1); - } + bool diving = program.get("--diving")[0] == 't'; + int gpu_id = program.get("--gpu"); if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); @@ -495,6 +489,9 @@ int main(int argc, char* argv[]) } merge_result_files(out_dir, result_file, n_gpus, batch_num); } else { + RAFT_CUDA_TRY(cudaSetDevice(gpu_id)); + CUOPT_LOG_INFO("Using GPU %d", gpu_id); + auto memory_resource = make_async(); rmm::mr::set_current_device_resource(memory_resource.get()); run_single_file(path, diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 6c99b2a8a4..9ef55a5ac9 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -35,14 +35,8 @@ struct benchmark_info_t { double objective_of_initial_population = std::numeric_limits::max(); }; -struct bnb_search_strategy_t { - enum { - BEST_FIRST = 0, - DEPTH_FIRST = 1, - BEST_FIRST_WITH_DIVING = 2 - }; - - int strategy = BEST_FIRST; +struct diving_settings_t { + bool use_diving = false; }; // Forward declare solver_settings_t for friend class @@ -89,7 +83,7 @@ class mip_solver_settings_t { f_t relative_mip_gap = 1.0e-4; }; - bnb_search_strategy_t search_strategy; + diving_settings_t diving_settings; /** * @brief Get the tolerance settings as a single structure. diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9e3612002f..4a1c7f4cbe 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -405,10 +405,10 @@ template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, - const bnb_search_strategy_t strategy) + const diving_settings_t strategy) : original_problem(user_problem), settings(solver_settings), - search_strategy(strategy), + diving_settings(strategy), original_lp(1, 1, 1) { start_time = tic(); @@ -429,466 +429,7 @@ branch_and_bound_t::branch_and_bound_t( } template -mip_status_t branch_and_bound_t::solve_dfs(mip_solution_t& solution) -{ - mip_status_t status = mip_status_t::UNSET; - mip_solution_t incumbent(original_lp.num_cols); - - // Handle initial guess - if (guess.size() != 0) { - std::vector crushed_guess; - crush_primal_solution(original_problem, original_lp, guess, new_slacks, crushed_guess); - f_t primal_err; - f_t bound_err; - i_t num_fractional; - const bool feasible = check_guess( - original_lp, settings, var_types, crushed_guess, primal_err, bound_err, num_fractional); - if (feasible) { - const f_t computed_obj = compute_objective(original_lp, crushed_guess); - global_variables::mutex_upper.lock(); - incumbent.set_incumbent_solution(computed_obj, crushed_guess); - global_variables::upper_bound = computed_obj; - global_variables::mutex_upper.unlock(); - } - } - - // Calculate the LP relaxation of the root node - lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); - std::vector root_vstatus; - std::vector edge_norms; - settings.log.printf("Solving LP root relaxation\n"); - simplex_solver_settings_t lp_settings = settings; - lp_settings.inside_mip = 1; - lp_status_t root_status = solve_linear_program_advanced( - original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); - f_t total_lp_solve_time = toc(start_time); - assert(root_vstatus.size() == original_lp.num_cols); - if (root_status == lp_status_t::INFEASIBLE) { - settings.log.printf("MIP Infeasible\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::INFEASIBLE; - } - if (root_status == lp_status_t::UNBOUNDED) { - settings.log.printf("MIP Unbounded\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::UNBOUNDED; - } - if (root_status == lp_status_t::TIME_LIMIT) { - settings.log.printf("Hit time limit\n"); - return mip_status_t::TIME_LIMIT; - } - set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); - - std::vector fractional; - const i_t num_fractional = - fractional_variables(settings, root_relax_soln.x, var_types, fractional); - const f_t root_objective = compute_objective(original_lp, root_relax_soln.x); - if (settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); - settings.set_simplex_solution_callback(original_x, - compute_user_objective(original_lp, root_objective)); - } - global_variables::mutex_lower.lock(); - f_t lower_bound = global_variables::lower_bound = root_objective; - global_variables::mutex_lower.unlock(); - - // Check if the MIP has already been solved at the root node - if (num_fractional == 0) { - global_variables::mutex_upper.lock(); - incumbent.set_incumbent_solution(root_objective, root_relax_soln.x); - global_variables::upper_bound = root_objective; - global_variables::mutex_upper.unlock(); - // We should be done here - uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); - solution.objective = incumbent.objective; - solution.lower_bound = lower_bound; - solution.nodes_explored = 0; - solution.simplex_iterations = root_relax_soln.iterations; - settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", - compute_user_objective(original_lp, root_objective), - toc(start_time)); - if (settings.solution_callback != nullptr) { - settings.solution_callback(solution.x, solution.objective); - } - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::OPTIMAL; - } - - // Apply strong branching as the initial pseudocost for the fractional variables - pseudo_costs_t pc(original_lp.num_cols); - strong_branching(original_lp, - settings, - start_time, - var_types, - root_relax_soln.x, - fractional, - root_objective, - root_vstatus, - edge_norms, - pc); - - i_t num_nodes = 0; - mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", lower_bound); - - std::vector*> node_stack; - - // Choose variable to branch on - logger_t log; - log.log = false; - const i_t branch_var = - pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); - - // down child - std::unique_ptr> down_child = - std::make_unique>(original_lp, - &root_node, - ++num_nodes, - branch_var, - 0, - root_relax_soln.x[branch_var], - root_vstatus); - - graphviz_edge(settings, - &root_node, - down_child.get(), - branch_var, - 0, - std::floor(root_relax_soln.x[branch_var])); - - // up child - std::unique_ptr> up_child = - std::make_unique>(original_lp, - &root_node, - ++num_nodes, - branch_var, - 1, - root_relax_soln.x[branch_var], - root_vstatus); - - graphviz_edge( - settings, &root_node, up_child.get(), branch_var, 0, std::ceil(root_relax_soln.x[branch_var])); - - assert(root_vstatus.size() == original_lp.num_cols); - node_stack.push_back(down_child.get()); // the stack does not own the unique_ptr the tree does - node_stack.push_back(up_child.get()); // the stack does not own the unqiue_ptr the tree does - root_node.add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree - lp_problem_t leaf_problem = - original_lp; // Make a copy of the original LP. We will modify its bounds at each leaf - f_t gap = get_upper_bound() - lower_bound; - i_t nodes_explored = 0; - settings.log.printf( - "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " - " Time \n"); - global_variables::mutex_branching.lock(); - global_variables::currently_branching = true; - global_variables::mutex_branching.unlock(); - - f_t total_lp_iters = 0.0; - f_t last_log = 0; - while (gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && - node_stack.size() > 0) { - // Check if there are any solutions to repair - std::vector> to_repair; - global_variables::mutex_repair.lock(); - if (global_variables::repair_queue.size() > 0) { - to_repair = global_variables::repair_queue; - global_variables::repair_queue.clear(); - } - global_variables::mutex_repair.unlock(); - - if (to_repair.size() > 0) { - settings.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); - for (const std::vector& potential_solution : to_repair) { - std::vector repaired_solution; - f_t repaired_obj; - bool is_feasible = repair_solution( - root_vstatus, edge_norms, potential_solution, repaired_obj, repaired_solution); - if (is_feasible) { - global_variables::mutex_upper.lock(); - if (repaired_obj < global_variables::upper_bound) { - global_variables::upper_bound = repaired_obj; - incumbent.set_incumbent_solution(repaired_obj, repaired_solution); - - settings.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", - compute_user_objective(original_lp, repaired_obj), - compute_user_objective(original_lp, lower_bound), - user_mip_gap(compute_user_objective(original_lp, repaired_obj), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - toc(start_time)); - if (settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, repaired_solution, original_x); - settings.solution_callback(original_x, repaired_obj); - } - } - global_variables::mutex_upper.unlock(); - } - } - } - - // Get a node off the stack - mip_node_t* node_ptr = node_stack.back(); - node_stack.pop_back(); // Remove node from the stack - f_t upper_bound = get_upper_bound(); - - gap = upper_bound - lower_bound; - const i_t leaf_depth = node_ptr->depth; - f_t now = toc(start_time); - f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || - nodes_explored < 1000) && - (time_since_log >= 1) || - (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - node_stack.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), - leaf_depth, - nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - now); - last_log = tic(); - } - - if (now > settings.time_limit) { - settings.log.printf("Hit time limit. Stoppping\n"); - status = mip_status_t::TIME_LIMIT; - break; - } - - // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp.lower; - leaf_problem.upper = original_lp.upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); - - std::vector& leaf_vstatus = node_ptr->vstatus; - lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); - - i_t node_iter = 0; - assert(leaf_vstatus.size() == leaf_problem.num_cols); - f_t lp_start_time = tic(); - std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; - - // Solve the LP relaxation of the current node using Dual Simplex and the previous basis - simplex_solver_settings_t lp_settings = settings; - lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings.dual_tol; - lp_settings.inside_mip = 2; - dual::status_t lp_status = dual_phase2(2, - 0, - lp_start_time, - leaf_problem, - lp_settings, - leaf_vstatus, - leaf_solution, - node_iter, - leaf_edge_norms); - if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", nodes_explored); - lp_status_t second_status = solve_linear_program_advanced( - leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); - lp_status = convert_lp_status_to_dual_status(second_status); - } - total_lp_solve_time += toc(lp_start_time); - total_lp_iters += node_iter; - - nodes_explored++; - if (lp_status == dual::status_t::DUAL_UNBOUNDED) { - node_ptr->lower_bound = inf; - std::vector*> stack; - node_ptr->set_status(node_status_t::INFEASIBLE, stack); - graphviz_node(settings, node_ptr, "infeasible", 0.0); - remove_fathomed_nodes(stack); - // Node was infeasible. Do not branch - } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = upper_bound; - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "cut off", leaf_objective); - remove_fathomed_nodes(stack); - // Node was cut off. Do not branch - - } else if (lp_status == dual::status_t::OPTIMAL) { - // LP was feasible - std::vector fractional; - const i_t leaf_fractional = - fractional_variables(settings, leaf_solution.x, var_types, fractional); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "lower bound", leaf_objective); - - pc.update_pseudo_costs(node_ptr, leaf_objective); - node_ptr->lower_bound = leaf_objective; - - constexpr f_t fathom_tol = 1e-5; - if (leaf_fractional == 0) { - bool send_solution = false; - global_variables::mutex_upper.lock(); - if (leaf_objective < global_variables::upper_bound) { - incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); - global_variables::upper_bound = upper_bound = leaf_objective; - gap = upper_bound - lower_bound; - settings.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - node_stack.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), - leaf_depth, - nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - toc(start_time)); - send_solution = true; - } - global_variables::mutex_upper.unlock(); - if (send_solution && settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); - settings.solution_callback(original_x, upper_bound); - } - graphviz_node(settings, node_ptr, "integer feasible", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); - remove_fathomed_nodes(stack); - - } else if (leaf_objective <= upper_bound + fathom_tol) { - // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - assert(leaf_vstatus.size() == leaf_problem.num_cols); - - // down child - std::unique_ptr> down_child = - std::make_unique>(original_lp, - node_ptr, - ++num_nodes, - branch_var, - 0, - leaf_solution.x[branch_var], - leaf_vstatus); - graphviz_edge(settings, - node_ptr, - down_child.get(), - branch_var, - 0, - std::floor(leaf_solution.x[branch_var])); - // up child - std::unique_ptr> up_child = - std::make_unique>(original_lp, - node_ptr, - ++num_nodes, - branch_var, - 1, - leaf_solution.x[branch_var], - leaf_vstatus); - graphviz_edge(settings, - node_ptr, - up_child.get(), - branch_var, - 0, - std::ceil(leaf_solution.x[branch_var])); - - // Martin's child selection - const f_t down_val = std::floor(root_relax_soln.x[branch_var]); - const f_t up_val = std::ceil(root_relax_soln.x[branch_var]); - const f_t down_dist = leaf_solution.x[branch_var] - down_val; - const f_t up_dist = up_val - leaf_solution.x[branch_var]; - - if (down_dist < up_dist) { - node_stack.push_back(up_child.get()); - node_stack.push_back(down_child.get()); - } else { - node_stack.push_back(up_child.get()); - node_stack.push_back(down_child.get()); - } - - node_ptr->add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree - } else { - graphviz_node(settings, node_ptr, "fathomed", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - remove_fathomed_nodes(stack); - } - } else { - graphviz_node(settings, node_ptr, "numerical", 0.0); - settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", - lp_status); - status = mip_status_t::NUMERICAL; - break; - } - } - global_variables::mutex_branching.lock(); - global_variables::currently_branching = false; - global_variables::mutex_branching.unlock(); - - if (node_stack.size() == 0) { - global_variables::mutex_lower.lock(); - lower_bound = global_variables::lower_bound = root_node.lower_bound; - global_variables::mutex_lower.unlock(); - gap = get_upper_bound() - lower_bound; - } - - settings.log.printf( - "Explored %d nodes in %.2fs.\nAbsolute Gap %e Objective %.16e Lower Bound %.16e\n", - nodes_explored, - toc(start_time), - gap, - compute_user_objective(original_lp, get_upper_bound()), - compute_user_objective(original_lp, lower_bound)); - - if (gap <= settings.absolute_mip_gap_tol || - relative_gap(get_upper_bound(), lower_bound) <= settings.relative_mip_gap_tol) { - status = mip_status_t::OPTIMAL; - if (gap > 0 && gap <= settings.absolute_mip_gap_tol) { - settings.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", - settings.absolute_mip_gap_tol); - } else if (gap > 0 && - relative_gap(get_upper_bound(), lower_bound) <= settings.relative_mip_gap_tol) { - settings.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", - settings.relative_mip_gap_tol); - } else { - settings.log.printf("Optimal solution found.\n"); - } - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - } - - if (node_stack.size() == 0 && get_upper_bound() == inf) { - settings.log.printf("Integer infeasible.\n"); - status = mip_status_t::INFEASIBLE; - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - } - - uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); - solution.objective = incumbent.objective; - solution.lower_bound = lower_bound; - solution.nodes_explored = nodes_explored; - solution.simplex_iterations = total_lp_iters; - return status; -} - -template -mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& solution) +mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; mip_solution_t incumbent(original_lp.num_cols); @@ -1007,7 +548,8 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s std::future diving_thread; - if (search_strategy.strategy == bnb_search_strategy_t::BEST_FIRST_WITH_DIVING) { + if (diving_settings.use_diving) { + settings.log.printf("Launching the diving thread!\n"); diving_thread = std::async(std::launch::async, [&]() { return diving( root_objective, edge_norms, pc, branch_var, branch_var_val, root_vstatus, incumbent); @@ -1281,7 +823,7 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s } } - if (search_strategy.strategy == bnb_search_strategy_t::BEST_FIRST_WITH_DIVING) { + if (diving_settings.use_diving) { mip_status_t diving_status = diving_thread.get(); } @@ -1339,13 +881,14 @@ mip_status_t branch_and_bound_t::solve_bfs(mip_solution_t& s } template -mip_status_t branch_and_bound_t::diving(f_t root_objective, - std::vector& edge_norms, - pseudo_costs_t pc, - i_t branch_var, - f_t branch_var_val, - const std::vector& root_vstatus, - mip_solution_t& incumbent) +mip_status_t branch_and_bound_t::diving( + f_t root_objective, + std::vector& edge_norms, + pseudo_costs_t pc, + i_t branch_var, + f_t branch_var_val, + const std::vector& root_vstatus, + mip_solution_t& incumbent) { logger_t log; log.log = false; @@ -1358,25 +901,12 @@ mip_status_t branch_and_bound_t::diving(f_t root_objective, i_t num_nodes = 1; // down child - std::unique_ptr> down_child = - std::make_unique>(original_lp, - &root_node, - ++num_nodes, - branch_var, - 0, - branch_var_val, - root_vstatus); + std::unique_ptr> down_child = std::make_unique>( + original_lp, &root_node, ++num_nodes, branch_var, 0, branch_var_val, root_vstatus); // up child - std::unique_ptr> up_child = - std::make_unique>(original_lp, - &root_node, - ++num_nodes, - branch_var, - 1, - branch_var_val, - root_vstatus); - + std::unique_ptr> up_child = std::make_unique>( + original_lp, &root_node, ++num_nodes, branch_var, 1, branch_var_val, root_vstatus); node_stack.push_back(down_child.get()); // the stack does not own the unique_ptr the tree does node_stack.push_back(up_child.get()); // the stack does not own the unqiue_ptr the tree does @@ -1579,29 +1109,6 @@ mip_status_t branch_and_bound_t::diving(f_t root_objective, return status; } -template -mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) -{ - switch (search_strategy.strategy) { - case bnb_search_strategy_t::BEST_FIRST: - settings.log.printf("Using BFS as search strategy.\n"); - return solve_bfs(solution); - - case bnb_search_strategy_t::DEPTH_FIRST: - settings.log.printf("Using DFS as search strategy.\n"); - return solve_dfs(solution); - - case bnb_search_strategy_t::BEST_FIRST_WITH_DIVING: - settings.log.printf("Using BFS with diving as search strategy.\n"); - return solve_bfs(solution); - - default: - settings.log.printf("Invalid search strategy! Exiting..."); - exit(EXIT_FAILURE); - break; - } -} - #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template class branch_and_bound_t; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index d6076780a0..0ab9780a32 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -48,7 +48,7 @@ class branch_and_bound_t { public: branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, - const bnb_search_strategy_t strategy); + const diving_settings_t strategy); // Set an initial guess based on the user_problem. This should be called before solve. void set_initial_guess(const std::vector& user_guess) { guess = user_guess; } @@ -68,7 +68,7 @@ class branch_and_bound_t { private: const user_problem_t& original_problem; const simplex_solver_settings_t settings; - bnb_search_strategy_t search_strategy; + diving_settings_t diving_settings; f_t start_time; std::vector guess; @@ -77,10 +77,6 @@ class branch_and_bound_t { std::vector new_slacks; std::vector var_types; - mip_status_t solve_bfs(mip_solution_t& solution); - mip_status_t solve_bfs_diving(mip_solution_t& solution); - mip_status_t solve_dfs(mip_solution_t& solution); - mip_status_t diving(f_t root_objective, std::vector& edge_norms, pseudo_costs_t pc, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 0747d947ef..77ee589834 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -266,12 +266,12 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, template i_t solve(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const bnb_search_strategy_t search_strategy, + const diving_settings_t diving_settings, std::vector& primal_solution) { i_t status; if (is_mip(problem) && !settings.relaxation) { - branch_and_bound_t branch_and_bound(problem, settings, search_strategy); + branch_and_bound_t branch_and_bound(problem, settings, diving_settings); mip_solution_t mip_solution(problem.num_cols); mip_status_t mip_status = branch_and_bound.solve(mip_solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -304,13 +304,13 @@ i_t solve(const user_problem_t& problem, template i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const bnb_search_strategy_t search_strategy, + const diving_settings_t diving_settings, const std::vector& guess, mip_solution_t& solution) { i_t status; if (is_mip(problem)) { - branch_and_bound_t branch_and_bound(problem, settings, search_strategy); + branch_and_bound_t branch_and_bound(problem, settings, diving_settings); branch_and_bound.set_initial_guess(guess); mip_status_t mip_status = branch_and_bound.solve(solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -351,13 +351,13 @@ template lp_status_t solve_linear_program(const user_problem_t& use template int solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, - const bnb_search_strategy_t search_strategy, + const diving_settings_t diving_settings, std::vector& primal_solution); template int solve_mip_with_guess( const user_problem_t& problem, const simplex_solver_settings_t& settings, - const bnb_search_strategy_t search_strategy, + const diving_settings_t diving_settings, const std::vector& guess, mip_solution_t& solution); diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 46303a8d62..fce026b8ce 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -68,14 +68,14 @@ i_t solve_mip(const user_problem_t& user_problem, mip_solution_t i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const bnb_search_strategy_t search_strategy, + const diving_settings_t diving_settings, const std::vector& guess, mip_solution_t& solution); template i_t solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, - const bnb_search_strategy_t search_strategy, + const diving_settings_t diving_settings, std::vector& primal_solution); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 72ccbd6195..f0bb65de36 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -69,7 +69,7 @@ template struct branch_and_bound_solution_helper_t { branch_and_bound_solution_helper_t(diversity_manager_t* dm, dual_simplex::simplex_solver_settings_t& settings) - : dm(dm), settings_(settings) {}; + : dm(dm), settings_(settings){}; void solution_callback(std::vector& solution, f_t objective) { @@ -190,7 +190,7 @@ solution_t mip_solver_t::run_solver() // Create the branch and bound object branch_and_bound = std::make_unique>( - branch_and_bound_problem, branch_and_bound_settings, context.settings.search_strategy); + branch_and_bound_problem, branch_and_bound_settings, context.settings.diving_settings); // Set the primal heuristics -> branch and bound callback context.problem_ptr->branch_and_bound_callback = diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 56d04f04b8..617d29ae64 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -88,7 +88,7 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::CONTINUOUS; user_problem.var_types[1] = dual_simplex::variable_type_t::CONTINUOUS; - bnb_search_strategy_t search_strategy; + diving_settings_t diving_settings; double start_time = dual_simplex::tic(); dual_simplex::simplex_solver_settings_t settings; @@ -103,7 +103,7 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::INTEGER; user_problem.var_types[1] = dual_simplex::variable_type_t::INTEGER; - EXPECT_EQ((dual_simplex::solve(user_problem, settings, search_strategy, solution.x)), 0); + EXPECT_EQ((dual_simplex::solve(user_problem, settings, diving_settings, solution.x)), 0); } TEST(dual_simplex, burglar) @@ -164,11 +164,13 @@ TEST(dual_simplex, burglar) user_problem.var_types[j] = cuopt::linear_programming::dual_simplex::variable_type_t::INTEGER; } - bnb_search_strategy_t search_strategy; + diving_settings_t diving_settings; cuopt::linear_programming::dual_simplex::simplex_solver_settings_t settings; std::vector solution(num_items); - EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve(user_problem, settings, search_strategy, solution)), 0); + EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve( + user_problem, settings, diving_settings, solution)), + 0); double objective = 0.0; for (int j = 0; j < num_items; ++j) { objective += value[j] * solution[j]; From 73659d2f2865a993d06b8993cf88cfadf2b8a2aa Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 21 Aug 2025 15:51:16 +0200 Subject: [PATCH 07/44] Shared pseudocost between main and diving threads --- cpp/src/dual_simplex/branch_and_bound.cpp | 64 +++++++++-------------- cpp/src/dual_simplex/branch_and_bound.hpp | 5 +- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 4a1c7f4cbe..f3175017d1 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -342,12 +342,11 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu } template -bool branch_and_bound_t::repair_solution( - const std::vector& root_vstatus, - const std::vector& edge_norms, - const std::vector& potential_solution, - f_t& repaired_obj, - std::vector& repaired_solution) const +bool branch_and_bound_t::repair_solution(const std::vector& root_vstatus, + const std::vector& edge_norms, + const std::vector& potential_solution, + f_t& repaired_obj, + std::vector& repaired_solution) const { bool feasible = false; repaired_obj = std::numeric_limits::quiet_NaN(); @@ -734,7 +733,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + pseudocost_mutex.lock(); pc.update_pseudo_costs(node_ptr, leaf_objective); + pseudocost_mutex.unlock(); + node_ptr->lower_bound = leaf_objective; constexpr f_t fathom_tol = 1e-5; @@ -770,8 +772,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut remove_fathomed_nodes(stack); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + pseudocost_mutex.lock(); + const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + pseudocost_mutex.unlock(); assert(leaf_vstatus.size() == leaf_problem.num_cols); // down child @@ -823,9 +826,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - if (diving_settings.use_diving) { - mip_status_t diving_status = diving_thread.get(); - } + if (diving_settings.use_diving) { mip_status_t diving_status = diving_thread.get(); } global_variables::mutex_branching.lock(); global_variables::currently_branching = false; @@ -881,14 +882,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } template -mip_status_t branch_and_bound_t::diving( - f_t root_objective, - std::vector& edge_norms, - pseudo_costs_t pc, - i_t branch_var, - f_t branch_var_val, - const std::vector& root_vstatus, - mip_solution_t& incumbent) +mip_status_t branch_and_bound_t::diving(f_t root_objective, + std::vector& edge_norms, + pseudo_costs_t& pc, + i_t branch_var, + f_t branch_var_val, + const std::vector& root_vstatus, + mip_solution_t& incumbent) { logger_t log; log.log = false; @@ -932,24 +932,6 @@ mip_status_t branch_and_bound_t::diving( f_t now = toc(start_time); f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - // if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || - // nodes_explored < 1000) && - // (time_since_log >= 1) || - // (time_since_log > 60) || now > settings.time_limit) { - // settings.log.printf("D%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - // nodes_explored, - // node_stack.size(), - // compute_user_objective(original_lp, upper_bound), - // compute_user_objective(original_lp, lower_bound), - // leaf_depth, - // nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, - // user_mip_gap(compute_user_objective(original_lp, upper_bound), - // compute_user_objective(original_lp, lower_bound)) - // .c_str(), - // now); - // last_log = tic(); - // } - if (now > settings.time_limit) { status = mip_status_t::TIME_LIMIT; break; @@ -1017,7 +999,10 @@ mip_status_t branch_and_bound_t::diving( f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + pseudocost_mutex.lock(); pc.update_pseudo_costs(node_ptr, leaf_objective); + pseudocost_mutex.unlock(); + node_ptr->lower_bound = leaf_objective; constexpr f_t fathom_tol = 1e-5; @@ -1055,8 +1040,9 @@ mip_status_t branch_and_bound_t::diving( } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + pseudocost_mutex.lock(); + const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + pseudocost_mutex.unlock(); assert(leaf_vstatus.size() == leaf_problem.num_cols); // down child diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0ab9780a32..10d94f26d5 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include "cuopt/linear_programming/mip/solver_settings.hpp" @@ -77,9 +78,11 @@ class branch_and_bound_t { std::vector new_slacks; std::vector var_types; + std::mutex pseudocost_mutex; + mip_status_t diving(f_t root_objective, std::vector& edge_norms, - pseudo_costs_t pc, + pseudo_costs_t& pc, i_t branch_var, f_t branch_var_val, const std::vector& root_vstatus, From 7064489d5d6aab3baa718745a4a6649dbee9e634 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 21 Aug 2025 19:49:51 +0200 Subject: [PATCH 08/44] Undo sharing pseudocost (due to regression) --- cpp/src/dual_simplex/branch_and_bound.cpp | 11 +---------- cpp/src/dual_simplex/branch_and_bound.hpp | 4 +--- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index f3175017d1..996373ee67 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -733,10 +733,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); graphviz_node(settings, node_ptr, "lower bound", leaf_objective); - pseudocost_mutex.lock(); pc.update_pseudo_costs(node_ptr, leaf_objective); - pseudocost_mutex.unlock(); - node_ptr->lower_bound = leaf_objective; constexpr f_t fathom_tol = 1e-5; @@ -772,9 +769,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut remove_fathomed_nodes(stack); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - pseudocost_mutex.lock(); const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - pseudocost_mutex.unlock(); assert(leaf_vstatus.size() == leaf_problem.num_cols); // down child @@ -884,7 +879,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut template mip_status_t branch_and_bound_t::diving(f_t root_objective, std::vector& edge_norms, - pseudo_costs_t& pc, + pseudo_costs_t pc, i_t branch_var, f_t branch_var_val, const std::vector& root_vstatus, @@ -999,9 +994,7 @@ mip_status_t branch_and_bound_t::diving(f_t root_objective, f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); graphviz_node(settings, node_ptr, "lower bound", leaf_objective); - pseudocost_mutex.lock(); pc.update_pseudo_costs(node_ptr, leaf_objective); - pseudocost_mutex.unlock(); node_ptr->lower_bound = leaf_objective; @@ -1040,9 +1033,7 @@ mip_status_t branch_and_bound_t::diving(f_t root_objective, } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - pseudocost_mutex.lock(); const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - pseudocost_mutex.unlock(); assert(leaf_vstatus.size() == leaf_problem.num_cols); // down child diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 10d94f26d5..a184a51f88 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -78,11 +78,9 @@ class branch_and_bound_t { std::vector new_slacks; std::vector var_types; - std::mutex pseudocost_mutex; - mip_status_t diving(f_t root_objective, std::vector& edge_norms, - pseudo_costs_t& pc, + pseudo_costs_t pc, i_t branch_var, f_t branch_var_val, const std::vector& root_vstatus, From 180ec690d5db1f2bd309e0d712bdeb20102cd6b5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 14:59:31 +0200 Subject: [PATCH 09/44] Extracted repair solution from bnb. --- cpp/src/dual_simplex/branch_and_bound.cpp | 160 ++++++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 7 + 2 files changed, 98 insertions(+), 69 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 996373ee67..0bc2dc3257 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -342,11 +342,12 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu } template -bool branch_and_bound_t::repair_solution(const std::vector& root_vstatus, - const std::vector& edge_norms, - const std::vector& potential_solution, - f_t& repaired_obj, - std::vector& repaired_solution) const +bool branch_and_bound_t::repair_solution( + const std::vector& root_vstatus, + const std::vector& edge_norms, + const std::vector& potential_solution, + f_t& repaired_obj, + std::vector& repaired_solution) const { bool feasible = false; repaired_obj = std::numeric_limits::quiet_NaN(); @@ -427,6 +428,60 @@ branch_and_bound_t::branch_and_bound_t( global_variables::mutex_branching.unlock(); } +template +void branch_and_bound_t::repair_heuristic_solutions(const std::vector& root_vstatus, + const std::vector& edge_norms, + const f_t& lower_bound, + mip_solution_t& incumbent, + mip_solution_t& solution) +{ + // Check if there are any solutions to repair + std::vector> to_repair; + global_variables::mutex_repair.lock(); + if (global_variables::repair_queue.size() > 0) { + to_repair = global_variables::repair_queue; + global_variables::repair_queue.clear(); + } + global_variables::mutex_repair.unlock(); + + if (to_repair.size() > 0) { + settings.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); + for (const std::vector& potential_solution : to_repair) { + std::vector repaired_solution; + f_t repaired_obj; + bool is_feasible = repair_solution( + root_vstatus, edge_norms, potential_solution, repaired_obj, repaired_solution); + if (is_feasible) { + global_variables::mutex_upper.lock(); + + if (repaired_obj < global_variables::upper_bound) { + global_variables::upper_bound = repaired_obj; + incumbent.set_incumbent_solution(repaired_obj, repaired_solution); + + f_t obj = compute_user_objective(original_lp, repaired_obj); + f_t lower = compute_user_objective(original_lp, lower_bound); + std::string gap = user_mip_gap(obj, lower); + + settings.log.printf( + "H %+13.6e %+10.6e %s %9.2f\n", + obj, + lower, + gap.c_str(), + toc(start_time)); + + if (settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, repaired_solution, original_x); + settings.solution_callback(original_x, repaired_obj); + } + } + + global_variables::mutex_upper.unlock(); + } + } + } +} + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -528,16 +583,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut edge_norms, pc); - auto compare = [](mip_node_t* a, mip_node_t* b) { - return a->lower_bound > - b->lower_bound; // True if a comes before b, elements that come before are output last - }; - std::priority_queue*, std::vector*>, decltype(compare)> - heap(compare); - i_t num_nodes = 0; - mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", lower_bound); - // Choose variable to branch on logger_t log; log.log = false; @@ -555,6 +600,25 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut }); } + settings.log.printf( + "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " + " Time \n"); + global_variables::mutex_branching.lock(); + global_variables::currently_branching = true; + global_variables::mutex_branching.unlock(); + + auto compare = [](mip_node_t* a, mip_node_t* b) { + return a->lower_bound > + b->lower_bound; // True if a comes before b, elements that come before are output last + }; + + i_t num_nodes = 0; + std::priority_queue*, std::vector*>, decltype(compare)> + heap(compare); + + mip_node_t root_node(root_objective, root_vstatus); + graphviz_node(settings, &root_node, "lower bound", lower_bound); + // down child std::unique_ptr> down_child = std::make_unique>( original_lp, &root_node, ++num_nodes, branch_var, 0, branch_var_val, root_vstatus); @@ -578,58 +642,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t gap = get_upper_bound() - lower_bound; i_t nodes_explored = 0; - settings.log.printf( - "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " - " Time \n"); - global_variables::mutex_branching.lock(); - global_variables::currently_branching = true; - global_variables::mutex_branching.unlock(); + f_t total_lp_iters = 0.0; f_t last_log = 0; while (gap > settings.absolute_mip_gap_tol && relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && heap.size() > 0) { - // Check if there are any solutions to repair - std::vector> to_repair; - global_variables::mutex_repair.lock(); - if (global_variables::repair_queue.size() > 0) { - to_repair = global_variables::repair_queue; - global_variables::repair_queue.clear(); - } - global_variables::mutex_repair.unlock(); - - if (to_repair.size() > 0) { - settings.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); - for (const std::vector& potential_solution : to_repair) { - std::vector repaired_solution; - f_t repaired_obj; - bool is_feasible = repair_solution( - root_vstatus, edge_norms, potential_solution, repaired_obj, repaired_solution); - if (is_feasible) { - global_variables::mutex_upper.lock(); - if (repaired_obj < global_variables::upper_bound) { - global_variables::upper_bound = repaired_obj; - incumbent.set_incumbent_solution(repaired_obj, repaired_solution); - - settings.log.printf( - "H %+13.6e %+10.6e %s %9.2f\n", - compute_user_objective(original_lp, repaired_obj), - compute_user_objective(original_lp, lower_bound), - user_mip_gap(compute_user_objective(original_lp, repaired_obj), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - toc(start_time)); - if (settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, repaired_solution, original_x); - settings.solution_callback(original_x, repaired_obj); - } - } - global_variables::mutex_upper.unlock(); - } - } - } + repair_heuristic_solutions(root_vstatus, edge_norms, lower_bound, incumbent, solution); // Get a node off the heap mip_node_t* node_ptr = heap.top(); @@ -877,13 +897,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } template -mip_status_t branch_and_bound_t::diving(f_t root_objective, - std::vector& edge_norms, - pseudo_costs_t pc, - i_t branch_var, - f_t branch_var_val, - const std::vector& root_vstatus, - mip_solution_t& incumbent) +mip_status_t branch_and_bound_t::diving( + f_t root_objective, + std::vector& edge_norms, + pseudo_costs_t pc, + i_t branch_var, + f_t branch_var_val, + const std::vector& root_vstatus, + mip_solution_t& incumbent) { logger_t log; log.log = false; @@ -1033,7 +1054,8 @@ mip_status_t branch_and_bound_t::diving(f_t root_objective, } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); assert(leaf_vstatus.size() == leaf_problem.num_cols); // down child diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index a184a51f88..0760e766c7 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -78,6 +78,13 @@ class branch_and_bound_t { std::vector new_slacks; std::vector var_types; + + void repair_heuristic_solutions(const std::vector& root_vstatus, + const std::vector& edge_norms, + const f_t& lower_bound, + mip_solution_t& incumbent, + mip_solution_t& solution); + mip_status_t diving(f_t root_objective, std::vector& edge_norms, pseudo_costs_t pc, From 0df95ba83b3d93322648267025a8b797e271182f Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 15:38:40 +0200 Subject: [PATCH 10/44] Aggregated most local variables into a struct. --- cpp/src/dual_simplex/branch_and_bound.cpp | 105 +++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 10 +++ 2 files changed, 63 insertions(+), 52 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 0bc2dc3257..9e893d7aa0 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -485,7 +485,9 @@ void branch_and_bound_t::repair_heuristic_solutions(const std::vector< template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { - mip_status_t status = mip_status_t::UNSET; + stats_t stats; + stats.status = mip_status_t::UNSET; + mip_solution_t incumbent(original_lp.num_cols); if (guess.size() != 0) { std::vector crushed_guess; @@ -512,7 +514,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lp_settings.inside_mip = 1; lp_status_t root_status = solve_linear_program_advanced( original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); - f_t total_lp_solve_time = toc(start_time); + stats.total_lp_solve_time = toc(start_time); assert(root_vstatus.size() == original_lp.num_cols); if (root_status == lp_status_t::INFEASIBLE) { settings.log.printf("MIP Infeasible\n"); @@ -545,7 +547,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut compute_user_objective(original_lp, root_objective)); } global_variables::mutex_lower.lock(); - f_t lower_bound = global_variables::lower_bound = root_objective; + stats.lower_bound = global_variables::lower_bound = root_objective; global_variables::mutex_lower.unlock(); if (num_fractional == 0) { @@ -556,7 +558,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // We should be done here uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); solution.objective = incumbent.objective; - solution.lower_bound = lower_bound; + solution.lower_bound = stats.lower_bound; solution.nodes_explored = 0; solution.simplex_iterations = root_relax_soln.iterations; settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", @@ -612,22 +614,22 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut b->lower_bound; // True if a comes before b, elements that come before are output last }; - i_t num_nodes = 0; + stats.num_nodes = 0; std::priority_queue*, std::vector*>, decltype(compare)> heap(compare); mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", lower_bound); + graphviz_node(settings, &root_node, "lower bound", stats.lower_bound); // down child std::unique_ptr> down_child = std::make_unique>( - original_lp, &root_node, ++num_nodes, branch_var, 0, branch_var_val, root_vstatus); + original_lp, &root_node, ++stats.num_nodes, branch_var, 0, branch_var_val, root_vstatus); graphviz_edge(settings, &root_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); // up child std::unique_ptr> up_child = std::make_unique>( - original_lp, &root_node, ++num_nodes, branch_var, 1, branch_var_val, root_vstatus); + original_lp, &root_node, ++stats.num_nodes, branch_var, 1, branch_var_val, root_vstatus); graphviz_edge(settings, &root_node, up_child.get(), branch_var, 0, std::ceil(branch_var_val)); @@ -640,16 +642,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp; - f_t gap = get_upper_bound() - lower_bound; - i_t nodes_explored = 0; - + stats.gap = get_upper_bound() - stats.lower_bound; + stats.nodes_explored = 0; - f_t total_lp_iters = 0.0; + stats.total_lp_iters = 0.0; f_t last_log = 0; - while (gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && + while (stats.gap > settings.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), stats.lower_bound) > settings.relative_mip_gap_tol && heap.size() > 0) { - repair_heuristic_solutions(root_vstatus, edge_norms, lower_bound, incumbent, solution); + repair_heuristic_solutions(root_vstatus, edge_norms, stats.lower_bound, incumbent, solution); // Get a node off the heap mip_node_t* node_ptr = heap.top(); @@ -665,25 +666,25 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut continue; } global_variables::mutex_lower.lock(); - global_variables::lower_bound = lower_bound = node_ptr->lower_bound; + global_variables::lower_bound = stats.lower_bound = node_ptr->lower_bound; global_variables::mutex_lower.unlock(); - gap = upper_bound - lower_bound; + stats.gap = upper_bound - stats.lower_bound; const i_t leaf_depth = node_ptr->depth; f_t now = toc(start_time); f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || - nodes_explored < 1000) && + if ((stats.nodes_explored % 1000 == 0 || stats.gap < 10 * settings.absolute_mip_gap_tol || + stats.nodes_explored < 1000) && (time_since_log >= 1) || (time_since_log > 60) || now > settings.time_limit) { settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, + stats.nodes_explored, heap.size(), compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), + compute_user_objective(original_lp, stats.lower_bound), leaf_depth, - nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, + stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) + compute_user_objective(original_lp, stats.lower_bound)) .c_str(), now); last_log = tic(); @@ -691,7 +692,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (now > settings.time_limit) { settings.log.printf("Hit time limit. Stoppping\n"); - status = mip_status_t::TIME_LIMIT; + stats.status = mip_status_t::TIME_LIMIT; break; } @@ -721,15 +722,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", nodes_explored); + settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", stats.nodes_explored); lp_status_t second_status = solve_linear_program_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } - total_lp_solve_time += toc(lp_start_time); - total_lp_iters += node_iter; + stats.total_lp_solve_time += toc(lp_start_time); + stats.total_lp_iters += node_iter; - nodes_explored++; + stats.nodes_explored++; if (lp_status == dual::status_t::DUAL_UNBOUNDED) { node_ptr->lower_bound = inf; std::vector*> stack; @@ -763,16 +764,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (leaf_objective < global_variables::upper_bound) { incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); global_variables::upper_bound = upper_bound = leaf_objective; - gap = upper_bound - lower_bound; + stats.gap = upper_bound - stats.lower_bound; settings.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, + stats.nodes_explored, heap.size(), compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), + compute_user_objective(original_lp, stats.lower_bound), leaf_depth, - nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, + stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) + compute_user_objective(original_lp, stats.lower_bound)) .c_str(), toc(start_time)); send_solution = true; @@ -796,7 +797,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::unique_ptr> down_child = std::make_unique>(original_lp, node_ptr, - ++num_nodes, + ++stats.num_nodes, branch_var, 0, leaf_solution.x[branch_var], @@ -811,7 +812,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::unique_ptr> up_child = std::make_unique>(original_lp, node_ptr, - ++num_nodes, + ++stats.num_nodes, branch_var, 1, leaf_solution.x[branch_var], @@ -836,7 +837,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut graphviz_node(settings, node_ptr, "numerical", 0.0); settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", lp_status); - status = mip_status_t::NUMERICAL; + stats.status = mip_status_t::NUMERICAL; break; } } @@ -849,27 +850,27 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (heap.size() == 0) { global_variables::mutex_lower.lock(); - lower_bound = global_variables::lower_bound = root_node.lower_bound; + stats.lower_bound = global_variables::lower_bound = root_node.lower_bound; global_variables::mutex_lower.unlock(); - gap = get_upper_bound() - lower_bound; + stats.gap = get_upper_bound() - stats.lower_bound; } settings.log.printf( "Explored %d nodes in %.2fs.\nAbsolute Gap %e Objective %.16e Lower Bound %.16e\n", - nodes_explored, + stats.nodes_explored, toc(start_time), - gap, + stats.gap, compute_user_objective(original_lp, get_upper_bound()), - compute_user_objective(original_lp, lower_bound)); + compute_user_objective(original_lp, stats.lower_bound)); - if (gap <= settings.absolute_mip_gap_tol || - relative_gap(get_upper_bound(), lower_bound) <= settings.relative_mip_gap_tol) { - status = mip_status_t::OPTIMAL; - if (gap > 0 && gap <= settings.absolute_mip_gap_tol) { + if (stats.gap <= settings.absolute_mip_gap_tol || + relative_gap(get_upper_bound(), stats.lower_bound) <= settings.relative_mip_gap_tol) { + stats.status = mip_status_t::OPTIMAL; + if (stats.gap > 0 && stats.gap <= settings.absolute_mip_gap_tol) { settings.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", settings.absolute_mip_gap_tol); - } else if (gap > 0 && - relative_gap(get_upper_bound(), lower_bound) <= settings.relative_mip_gap_tol) { + } else if (stats.gap > 0 && + relative_gap(get_upper_bound(), stats.lower_bound) <= settings.relative_mip_gap_tol) { settings.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", settings.relative_mip_gap_tol); } else { @@ -882,7 +883,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (heap.size() == 0 && get_upper_bound() == inf) { settings.log.printf("Integer infeasible.\n"); - status = mip_status_t::INFEASIBLE; + stats.status = mip_status_t::INFEASIBLE; if (settings.heuristic_preemption_callback != nullptr) { settings.heuristic_preemption_callback(); } @@ -890,10 +891,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); solution.objective = incumbent.objective; - solution.lower_bound = lower_bound; - solution.nodes_explored = nodes_explored; - solution.simplex_iterations = total_lp_iters; - return status; + solution.lower_bound = stats.lower_bound; + solution.nodes_explored = stats.nodes_explored; + solution.simplex_iterations = stats.total_lp_iters; + return stats.status; } template diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0760e766c7..25add5da56 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -78,6 +78,16 @@ class branch_and_bound_t { std::vector new_slacks; std::vector var_types; + struct stats_t { + f_t lower_bound = 0.0; + f_t gap = 0.0; + f_t total_lp_solve_time = 0.0; + i_t nodes_explored = 0; + i_t unexplored_nodes = 0; + f_t total_lp_iters = 0; + i_t num_nodes = 0; + mip_status_t status = mip_status_t::UNSET; + }; void repair_heuristic_solutions(const std::vector& root_vstatus, const std::vector& edge_norms, From 303eea2b237d00fc401c200a69368f31957c91fc Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 15:46:50 +0200 Subject: [PATCH 11/44] Moved best first search to a separated method --- cpp/src/dual_simplex/branch_and_bound.cpp | 349 ++++++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 24 +- 2 files changed, 207 insertions(+), 166 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 9e893d7aa0..a29343ceef 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -429,11 +429,12 @@ branch_and_bound_t::branch_and_bound_t( } template -void branch_and_bound_t::repair_heuristic_solutions(const std::vector& root_vstatus, - const std::vector& edge_norms, - const f_t& lower_bound, - mip_solution_t& incumbent, - mip_solution_t& solution) +void branch_and_bound_t::repair_heuristic_solutions( + const std::vector& root_vstatus, + const std::vector& edge_norms, + const f_t& lower_bound, + mip_solution_t& incumbent, + mip_solution_t& solution) { // Check if there are any solutions to repair std::vector> to_repair; @@ -483,131 +484,18 @@ void branch_and_bound_t::repair_heuristic_solutions(const std::vector< } template -mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) +void branch_and_bound_t::best_first_solve(stats_t& stats, + f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t& pc, + mip_solution_t& incumbent, + mip_solution_t& solution) { - stats_t stats; - stats.status = mip_status_t::UNSET; - - mip_solution_t incumbent(original_lp.num_cols); - if (guess.size() != 0) { - std::vector crushed_guess; - crush_primal_solution(original_problem, original_lp, guess, new_slacks, crushed_guess); - f_t primal_err; - f_t bound_err; - i_t num_fractional; - const bool feasible = check_guess( - original_lp, settings, var_types, crushed_guess, primal_err, bound_err, num_fractional); - if (feasible) { - const f_t computed_obj = compute_objective(original_lp, crushed_guess); - global_variables::mutex_upper.lock(); - incumbent.set_incumbent_solution(computed_obj, crushed_guess); - global_variables::upper_bound = computed_obj; - global_variables::mutex_upper.unlock(); - } - } - - lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); - std::vector root_vstatus; - std::vector edge_norms; - settings.log.printf("Solving LP root relaxation\n"); - simplex_solver_settings_t lp_settings = settings; - lp_settings.inside_mip = 1; - lp_status_t root_status = solve_linear_program_advanced( - original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); - stats.total_lp_solve_time = toc(start_time); - assert(root_vstatus.size() == original_lp.num_cols); - if (root_status == lp_status_t::INFEASIBLE) { - settings.log.printf("MIP Infeasible\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::INFEASIBLE; - } - if (root_status == lp_status_t::UNBOUNDED) { - settings.log.printf("MIP Unbounded\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::UNBOUNDED; - } - if (root_status == lp_status_t::TIME_LIMIT) { - settings.log.printf("Hit time limit\n"); - return mip_status_t::TIME_LIMIT; - } - set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); - - std::vector fractional; - const i_t num_fractional = - fractional_variables(settings, root_relax_soln.x, var_types, fractional); - const f_t root_objective = compute_objective(original_lp, root_relax_soln.x); - if (settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); - settings.set_simplex_solution_callback(original_x, - compute_user_objective(original_lp, root_objective)); - } - global_variables::mutex_lower.lock(); - stats.lower_bound = global_variables::lower_bound = root_objective; - global_variables::mutex_lower.unlock(); - - if (num_fractional == 0) { - global_variables::mutex_upper.lock(); - incumbent.set_incumbent_solution(root_objective, root_relax_soln.x); - global_variables::upper_bound = root_objective; - global_variables::mutex_upper.unlock(); - // We should be done here - uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); - solution.objective = incumbent.objective; - solution.lower_bound = stats.lower_bound; - solution.nodes_explored = 0; - solution.simplex_iterations = root_relax_soln.iterations; - settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", - compute_user_objective(original_lp, root_objective), - toc(start_time)); - if (settings.solution_callback != nullptr) { - settings.solution_callback(solution.x, solution.objective); - } - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::OPTIMAL; - } - - pseudo_costs_t pc(original_lp.num_cols); - strong_branching(original_lp, - settings, - start_time, - var_types, - root_relax_soln.x, - fractional, - root_objective, - root_vstatus, - edge_norms, - pc); - - // Choose variable to branch on logger_t log; log.log = false; - i_t branch_var = - pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); - f_t branch_var_val = root_relax_soln.x[branch_var]; - - std::future diving_thread; - - if (diving_settings.use_diving) { - settings.log.printf("Launching the diving thread!\n"); - diving_thread = std::async(std::launch::async, [&]() { - return diving( - root_objective, edge_norms, pc, branch_var, branch_var_val, root_vstatus, incumbent); - }); - } - - settings.log.printf( - "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " - " Time \n"); - global_variables::mutex_branching.lock(); - global_variables::currently_branching = true; - global_variables::mutex_branching.unlock(); auto compare = [](mip_node_t* a, mip_node_t* b) { return a->lower_bound > @@ -646,7 +534,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut stats.nodes_explored = 0; stats.total_lp_iters = 0.0; - f_t last_log = 0; + f_t last_log = 0; while (stats.gap > settings.absolute_mip_gap_tol && relative_gap(get_upper_bound(), stats.lower_bound) > settings.relative_mip_gap_tol && heap.size() > 0) { @@ -668,7 +556,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut global_variables::mutex_lower.lock(); global_variables::lower_bound = stats.lower_bound = node_ptr->lower_bound; global_variables::mutex_lower.unlock(); - stats.gap = upper_bound - stats.lower_bound; + stats.gap = upper_bound - stats.lower_bound; const i_t leaf_depth = node_ptr->depth; f_t now = toc(start_time); f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); @@ -676,17 +564,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut stats.nodes_explored < 1000) && (time_since_log >= 1) || (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - stats.nodes_explored, - heap.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, stats.lower_bound), - leaf_depth, - stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, stats.lower_bound)) - .c_str(), - now); + settings.log.printf( + " %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + stats.nodes_explored, + heap.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, stats.lower_bound), + leaf_depth, + stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, stats.lower_bound)) + .c_str(), + now); last_log = tic(); } @@ -722,7 +611,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", stats.nodes_explored); + settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", + stats.nodes_explored); lp_status_t second_status = solve_linear_program_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); @@ -764,18 +654,19 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (leaf_objective < global_variables::upper_bound) { incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); global_variables::upper_bound = upper_bound = leaf_objective; - stats.gap = upper_bound - stats.lower_bound; - settings.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - stats.nodes_explored, - heap.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, stats.lower_bound), - leaf_depth, - stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, stats.lower_bound)) - .c_str(), - toc(start_time)); + stats.gap = upper_bound - stats.lower_bound; + settings.log.printf( + "B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + stats.nodes_explored, + heap.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, stats.lower_bound), + leaf_depth, + stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, stats.lower_bound)) + .c_str(), + toc(start_time)); send_solution = true; } global_variables::mutex_upper.unlock(); @@ -790,7 +681,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut remove_fathomed_nodes(stack); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); assert(leaf_vstatus.size() == leaf_problem.num_cols); // down child @@ -842,15 +734,154 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } + stats.unexplored_nodes = heap.size(); +} + +template +mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) +{ + stats_t stats; + stats.status = mip_status_t::UNSET; + + mip_solution_t incumbent(original_lp.num_cols); + if (guess.size() != 0) { + std::vector crushed_guess; + crush_primal_solution(original_problem, original_lp, guess, new_slacks, crushed_guess); + f_t primal_err; + f_t bound_err; + i_t num_fractional; + const bool feasible = check_guess( + original_lp, settings, var_types, crushed_guess, primal_err, bound_err, num_fractional); + if (feasible) { + const f_t computed_obj = compute_objective(original_lp, crushed_guess); + global_variables::mutex_upper.lock(); + incumbent.set_incumbent_solution(computed_obj, crushed_guess); + global_variables::upper_bound = computed_obj; + global_variables::mutex_upper.unlock(); + } + } + + lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); + std::vector root_vstatus; + std::vector edge_norms; + settings.log.printf("Solving LP root relaxation\n"); + simplex_solver_settings_t lp_settings = settings; + lp_settings.inside_mip = 1; + lp_status_t root_status = solve_linear_program_advanced( + original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); + stats.total_lp_solve_time = toc(start_time); + assert(root_vstatus.size() == original_lp.num_cols); + if (root_status == lp_status_t::INFEASIBLE) { + settings.log.printf("MIP Infeasible\n"); + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::INFEASIBLE; + } + if (root_status == lp_status_t::UNBOUNDED) { + settings.log.printf("MIP Unbounded\n"); + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::UNBOUNDED; + } + if (root_status == lp_status_t::TIME_LIMIT) { + settings.log.printf("Hit time limit\n"); + return mip_status_t::TIME_LIMIT; + } + set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); + + std::vector fractional; + const i_t num_fractional = + fractional_variables(settings, root_relax_soln.x, var_types, fractional); + const f_t root_objective = compute_objective(original_lp, root_relax_soln.x); + if (settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); + settings.set_simplex_solution_callback(original_x, + compute_user_objective(original_lp, root_objective)); + } + global_variables::mutex_lower.lock(); + stats.lower_bound = global_variables::lower_bound = root_objective; + global_variables::mutex_lower.unlock(); + + if (num_fractional == 0) { + global_variables::mutex_upper.lock(); + incumbent.set_incumbent_solution(root_objective, root_relax_soln.x); + global_variables::upper_bound = root_objective; + global_variables::mutex_upper.unlock(); + // We should be done here + uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); + solution.objective = incumbent.objective; + solution.lower_bound = stats.lower_bound; + solution.nodes_explored = 0; + solution.simplex_iterations = root_relax_soln.iterations; + settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", + compute_user_objective(original_lp, root_objective), + toc(start_time)); + if (settings.solution_callback != nullptr) { + settings.solution_callback(solution.x, solution.objective); + } + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::OPTIMAL; + } + + pseudo_costs_t pc(original_lp.num_cols); + strong_branching(original_lp, + settings, + start_time, + var_types, + root_relax_soln.x, + fractional, + root_objective, + root_vstatus, + edge_norms, + pc); + + // Choose variable to branch on + logger_t log; + log.log = false; + i_t branch_var = + pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); + + settings.log.printf( + "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " + " Time \n"); + global_variables::mutex_branching.lock(); + global_variables::currently_branching = true; + global_variables::mutex_branching.unlock(); + + std::future diving_thread; + + if (diving_settings.use_diving) { + settings.log.printf("Launching the diving thread!\n"); + diving_thread = std::async(std::launch::async, [&]() { + return diving( + root_objective, edge_norms, pc, branch_var, root_relax_soln.x[branch_var], root_vstatus, incumbent); + }); + } + + best_first_solve(stats, + root_objective, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + edge_norms, + pc, + incumbent, + solution); + if (diving_settings.use_diving) { mip_status_t diving_status = diving_thread.get(); } global_variables::mutex_branching.lock(); global_variables::currently_branching = false; global_variables::mutex_branching.unlock(); - if (heap.size() == 0) { + if (stats.unexplored_nodes == 0) { global_variables::mutex_lower.lock(); - stats.lower_bound = global_variables::lower_bound = root_node.lower_bound; + stats.lower_bound = global_variables::lower_bound = root_objective; global_variables::mutex_lower.unlock(); stats.gap = get_upper_bound() - stats.lower_bound; } @@ -869,8 +900,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (stats.gap > 0 && stats.gap <= settings.absolute_mip_gap_tol) { settings.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", settings.absolute_mip_gap_tol); - } else if (stats.gap > 0 && - relative_gap(get_upper_bound(), stats.lower_bound) <= settings.relative_mip_gap_tol) { + } else if (stats.gap > 0 && relative_gap(get_upper_bound(), stats.lower_bound) <= + settings.relative_mip_gap_tol) { settings.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", settings.relative_mip_gap_tol); } else { @@ -881,7 +912,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - if (heap.size() == 0 && get_upper_bound() == inf) { + if (stats.unexplored_nodes == 0 && get_upper_bound() == inf) { settings.log.printf("Integer infeasible.\n"); stats.status = mip_status_t::INFEASIBLE; if (settings.heuristic_preemption_callback != nullptr) { diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 25add5da56..d68ff8bb52 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -79,14 +79,14 @@ class branch_and_bound_t { std::vector var_types; struct stats_t { - f_t lower_bound = 0.0; - f_t gap = 0.0; + f_t lower_bound = 0.0; + f_t gap = 0.0; f_t total_lp_solve_time = 0.0; - i_t nodes_explored = 0; - i_t unexplored_nodes = 0; - f_t total_lp_iters = 0; - i_t num_nodes = 0; - mip_status_t status = mip_status_t::UNSET; + i_t nodes_explored = 0; + i_t unexplored_nodes = 0; + f_t total_lp_iters = 0; + i_t num_nodes = 0; + mip_status_t status = mip_status_t::UNSET; }; void repair_heuristic_solutions(const std::vector& root_vstatus, @@ -95,6 +95,16 @@ class branch_and_bound_t { mip_solution_t& incumbent, mip_solution_t& solution); + void best_first_solve(stats_t& stats, + f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t& pc, + mip_solution_t& incumbent, + mip_solution_t& solution); + mip_status_t diving(f_t root_objective, std::vector& edge_norms, pseudo_costs_t pc, From 0e3ef5048cfe30704e681e094a312b94f077cf04 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 16:01:55 +0200 Subject: [PATCH 12/44] Moved branch logic to a separated method --- cpp/src/dual_simplex/branch_and_bound.cpp | 99 ++++++++++------------- cpp/src/dual_simplex/branch_and_bound.hpp | 6 ++ cpp/src/dual_simplex/mip_node.hpp | 10 +++ 3 files changed, 59 insertions(+), 56 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a29343ceef..360270e769 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -483,6 +483,30 @@ void branch_and_bound_t::repair_heuristic_solutions( } } +template +void branch_and_bound_t::branch(mip_node_t* parent_node, + i_t branch_var, + f_t branch_var_val, + std::vector& parent_vstatus, + stats_t& stats) +{ + // down child + std::unique_ptr> down_child = std::make_unique>( + original_lp, parent_node, ++stats.num_nodes, branch_var, 0, branch_var_val, parent_vstatus); + + graphviz_edge(settings, parent_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); + + // up child + std::unique_ptr> up_child = std::make_unique>( + original_lp, parent_node, ++stats.num_nodes, branch_var, 1, branch_var_val, parent_vstatus); + + graphviz_edge(settings, parent_node, up_child.get(), branch_var, 1, std::ceil(branch_var_val)); + + assert(parent_vstatus.size() == original_lp.num_cols); + parent_node->add_children(std::move(down_child), + std::move(up_child)); // child pointers moved into the tree +} + template void branch_and_bound_t::best_first_solve(stats_t& stats, f_t root_objective, @@ -509,23 +533,9 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, mip_node_t root_node(root_objective, root_vstatus); graphviz_node(settings, &root_node, "lower bound", stats.lower_bound); - // down child - std::unique_ptr> down_child = std::make_unique>( - original_lp, &root_node, ++stats.num_nodes, branch_var, 0, branch_var_val, root_vstatus); - - graphviz_edge(settings, &root_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); - - // up child - std::unique_ptr> up_child = std::make_unique>( - original_lp, &root_node, ++stats.num_nodes, branch_var, 1, branch_var_val, root_vstatus); - - graphviz_edge(settings, &root_node, up_child.get(), branch_var, 0, std::ceil(branch_var_val)); - - assert(root_vstatus.size() == original_lp.num_cols); - heap.push(down_child.get()); // the heap does not own the unique_ptr the tree does - heap.push(up_child.get()); // the heap does not own the unqiue_ptr the tree does - root_node.add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree + branch(&root_node, branch_var, branch_var_val, root_vstatus, stats); + heap.push(root_node.get_down_child()); // the heap does not own the unique_ptr the tree does + heap.push(root_node.get_up_child()); // the heap does not own the unqiue_ptr the tree does // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp; @@ -669,56 +679,28 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, toc(start_time)); send_solution = true; } - global_variables::mutex_upper.unlock(); + if (send_solution && settings.solution_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); settings.solution_callback(original_x, upper_bound); } + + global_variables::mutex_upper.unlock(); + graphviz_node(settings, node_ptr, "integer feasible", leaf_objective); std::vector*> stack; node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); remove_fathomed_nodes(stack); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); assert(leaf_vstatus.size() == leaf_problem.num_cols); - // down child - std::unique_ptr> down_child = - std::make_unique>(original_lp, - node_ptr, - ++stats.num_nodes, - branch_var, - 0, - leaf_solution.x[branch_var], - leaf_vstatus); - graphviz_edge(settings, - node_ptr, - down_child.get(), - branch_var, - 0, - std::floor(leaf_solution.x[branch_var])); - // up child - std::unique_ptr> up_child = - std::make_unique>(original_lp, - node_ptr, - ++stats.num_nodes, - branch_var, - 1, - leaf_solution.x[branch_var], - leaf_vstatus); - graphviz_edge(settings, - node_ptr, - up_child.get(), - branch_var, - 0, - std::ceil(leaf_solution.x[branch_var])); - heap.push(down_child.get()); // the heap does not own the unique_ptr the tree does - heap.push(up_child.get()); // the heap does not own the unique_ptr the tree does - node_ptr->add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree + branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, stats); + heap.push(node_ptr->get_down_child()); // the heap does not own the unique_ptr the tree does + heap.push(node_ptr->get_up_child()); // the heap does not own the unique_ptr the tree does + } else { graphviz_node(settings, node_ptr, "fathomed", leaf_objective); std::vector*> stack; @@ -858,8 +840,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (diving_settings.use_diving) { settings.log.printf("Launching the diving thread!\n"); diving_thread = std::async(std::launch::async, [&]() { - return diving( - root_objective, edge_norms, pc, branch_var, root_relax_soln.x[branch_var], root_vstatus, incumbent); + return diving(root_objective, + edge_norms, + pc, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + incumbent); }); } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index d68ff8bb52..a533fa7f55 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -105,6 +105,12 @@ class branch_and_bound_t { mip_solution_t& incumbent, mip_solution_t& solution); + void branch(mip_node_t* parent_node, + i_t branch_var, + f_t branch_var_val, + std::vector& parent_vstatus, + stats_t& stats); + mip_status_t diving(f_t root_objective, std::vector& edge_norms, pseudo_costs_t pc, diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 475a9efc6c..d32f48f850 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -99,6 +99,16 @@ class mip_node_t { } } + mip_node_t* get_down_child() const + { + return children[0].get(); + } + + mip_node_t* get_up_child() const + { + return children[1].get(); + } + void add_children(std::unique_ptr&& down_child, std::unique_ptr&& up_child) { From 7b0ce1ff5deaaf399eda2a782d99936aa47de467 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 16:34:57 +0200 Subject: [PATCH 13/44] Extracted adding feasible solution and solving leaf LP. --- cpp/src/dual_simplex/branch_and_bound.cpp | 273 +++++++++++++--------- cpp/src/dual_simplex/branch_and_bound.hpp | 22 ++ 2 files changed, 182 insertions(+), 113 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 360270e769..94ecbff18c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -507,6 +507,144 @@ void branch_and_bound_t::branch(mip_node_t* parent_node, std::move(up_child)); // child pointers moved into the tree } +template +void branch_and_bound_t::add_feasible_solution(mip_node_t* leaf_ptr, + f_t leaf_objective, + const std::vector& leaf_sol, + mip_solution_t& incumbent, + stats_t& stats, + char symbol) +{ + bool send_solution = false; + global_variables::mutex_upper.lock(); + if (leaf_objective < global_variables::upper_bound) { + incumbent.set_incumbent_solution(leaf_objective, leaf_sol); + global_variables::upper_bound = stats.upper_bound = leaf_objective; + stats.gap = stats.upper_bound - stats.lower_bound; + f_t obj = compute_user_objective(original_lp, stats.upper_bound); + f_t lower = compute_user_objective(original_lp, stats.lower_bound); + settings.log.printf("%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + symbol, + stats.nodes_explored, + stats.unexplored_nodes, + obj, + lower, + leaf_ptr->depth, + stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(start_time)); + send_solution = true; + } + + if (send_solution && settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); + settings.solution_callback(original_x, stats.upper_bound); + } + + global_variables::mutex_upper.unlock(); + + graphviz_node(settings, leaf_ptr, "integer feasible", leaf_objective); + std::vector*> stack; + leaf_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); + remove_fathomed_nodes(stack); +} + +template +mip_status_t branch_and_bound_t::solve_root_relaxation( + f_t& root_objective, + lp_solution_t& root_relax_soln, + std::vector& root_vstatus, + std::vector& edge_norms, + stats_t& stats) +{ + settings.log.printf("Solving LP root relaxation\n"); + simplex_solver_settings_t lp_settings = settings; + lp_settings.inside_mip = 1; + lp_status_t root_status = solve_linear_program_advanced( + original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); + stats.total_lp_solve_time = toc(start_time); + assert(root_vstatus.size() == original_lp.num_cols); + if (root_status == lp_status_t::INFEASIBLE) { + settings.log.printf("MIP Infeasible\n"); + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::INFEASIBLE; + } + if (root_status == lp_status_t::UNBOUNDED) { + settings.log.printf("MIP Unbounded\n"); + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } + return mip_status_t::UNBOUNDED; + } + if (root_status == lp_status_t::TIME_LIMIT) { + settings.log.printf("Hit time limit\n"); + return mip_status_t::TIME_LIMIT; + } + set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); + + std::vector fractional; + + root_objective = compute_objective(original_lp, root_relax_soln.x); + if (settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); + settings.set_simplex_solution_callback(original_x, + compute_user_objective(original_lp, root_objective)); + } + global_variables::mutex_lower.lock(); + stats.lower_bound = global_variables::lower_bound = root_objective; + global_variables::mutex_lower.unlock(); + + return mip_status_t::UNSET; +} + +template +dual::status_t branch_and_bound_t::solve_leaf_lp( + mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + std::vector& leaf_vstatus, + lp_solution_t& leaf_solution, + std::vector& edge_norms, + stats_t& stats) +{ + // Set the correct bounds for the leaf problem + leaf_problem.lower = original_lp.lower; + leaf_problem.upper = original_lp.upper; + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); + + i_t node_iter = 0; + assert(leaf_vstatus.size() == leaf_problem.num_cols); + f_t lp_start_time = tic(); + std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; + simplex_solver_settings_t lp_settings = settings; + lp_settings.set_log(false); + lp_settings.cut_off = stats.upper_bound + settings.dual_tol; + lp_settings.inside_mip = 2; + dual::status_t lp_status = dual_phase2(2, + 0, + lp_start_time, + leaf_problem, + lp_settings, + leaf_vstatus, + leaf_solution, + node_iter, + leaf_edge_norms); + if (lp_status == dual::status_t::NUMERICAL) { + settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", stats.nodes_explored); + lp_status_t second_status = solve_linear_program_advanced( + leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); + lp_status = convert_lp_status_to_dual_status(second_status); + } + stats.total_lp_solve_time += toc(lp_start_time); + stats.total_lp_iters += node_iter; + stats.nodes_explored++; + + return lp_status; +} + template void branch_and_bound_t::best_first_solve(stats_t& stats, f_t root_objective, @@ -535,14 +673,13 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, branch(&root_node, branch_var, branch_var_val, root_vstatus, stats); heap.push(root_node.get_down_child()); // the heap does not own the unique_ptr the tree does - heap.push(root_node.get_up_child()); // the heap does not own the unqiue_ptr the tree does + heap.push(root_node.get_up_child()); // the heap does not own the unqiue_ptr the tree does // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp; stats.gap = get_upper_bound() - stats.lower_bound; stats.nodes_explored = 0; - stats.total_lp_iters = 0.0; f_t last_log = 0; while (stats.gap > settings.absolute_mip_gap_tol && @@ -553,8 +690,8 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, // Get a node off the heap mip_node_t* node_ptr = heap.top(); heap.pop(); // Remove node from the heap - f_t upper_bound = get_upper_bound(); - if (upper_bound < node_ptr->lower_bound) { + stats.upper_bound = get_upper_bound(); + if (stats.upper_bound < node_ptr->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the current // upper bound std::vector*> stack; @@ -566,7 +703,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, global_variables::mutex_lower.lock(); global_variables::lower_bound = stats.lower_bound = node_ptr->lower_bound; global_variables::mutex_lower.unlock(); - stats.gap = upper_bound - stats.lower_bound; + stats.gap = stats.upper_bound - stats.lower_bound; const i_t leaf_depth = node_ptr->depth; f_t now = toc(start_time); f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); @@ -578,11 +715,11 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, " %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", stats.nodes_explored, heap.size(), - compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, stats.upper_bound), compute_user_objective(original_lp, stats.lower_bound), leaf_depth, stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), + user_mip_gap(compute_user_objective(original_lp, stats.upper_bound), compute_user_objective(original_lp, stats.lower_bound)) .c_str(), now); @@ -595,42 +732,11 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, break; } - // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp.lower; - leaf_problem.upper = original_lp.upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); - std::vector& leaf_vstatus = node_ptr->vstatus; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + dual::status_t lp_status = + solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, stats); - i_t node_iter = 0; - assert(leaf_vstatus.size() == leaf_problem.num_cols); - f_t lp_start_time = tic(); - std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; - simplex_solver_settings_t lp_settings = settings; - lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings.dual_tol; - lp_settings.inside_mip = 2; - dual::status_t lp_status = dual_phase2(2, - 0, - lp_start_time, - leaf_problem, - lp_settings, - leaf_vstatus, - leaf_solution, - node_iter, - leaf_edge_norms); - if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", - stats.nodes_explored); - lp_status_t second_status = solve_linear_program_advanced( - leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); - lp_status = convert_lp_status_to_dual_status(second_status); - } - stats.total_lp_solve_time += toc(lp_start_time); - stats.total_lp_iters += node_iter; - - stats.nodes_explored++; if (lp_status == dual::status_t::DUAL_UNBOUNDED) { node_ptr->lower_bound = inf; std::vector*> stack; @@ -639,7 +745,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, remove_fathomed_nodes(stack); // Node was infeasible. Do not branch } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = upper_bound; + node_ptr->lower_bound = stats.upper_bound; std::vector*> stack; node_ptr->set_status(node_status_t::FATHOMED, stack); f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); @@ -659,47 +765,19 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { - bool send_solution = false; - global_variables::mutex_upper.lock(); - if (leaf_objective < global_variables::upper_bound) { - incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); - global_variables::upper_bound = upper_bound = leaf_objective; - stats.gap = upper_bound - stats.lower_bound; - settings.log.printf( - "B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - stats.nodes_explored, - heap.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, stats.lower_bound), - leaf_depth, - stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, stats.lower_bound)) - .c_str(), - toc(start_time)); - send_solution = true; - } + stats.unexplored_nodes = heap.size(); + add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, stats, 'B'); - if (send_solution && settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); - settings.solution_callback(original_x, upper_bound); - } - - global_variables::mutex_upper.unlock(); - - graphviz_node(settings, node_ptr, "integer feasible", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); - remove_fathomed_nodes(stack); - } else if (leaf_objective <= upper_bound + fathom_tol) { + } else if (leaf_objective <= stats.upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection(fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); assert(leaf_vstatus.size() == leaf_problem.num_cols); branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, stats); - heap.push(node_ptr->get_down_child()); // the heap does not own the unique_ptr the tree does - heap.push(node_ptr->get_up_child()); // the heap does not own the unique_ptr the tree does + heap.push( + node_ptr->get_down_child()); // the heap does not own the unique_ptr the tree does + heap.push(node_ptr->get_up_child()); // the heap does not own the unique_ptr the tree does } else { graphviz_node(settings, node_ptr, "fathomed", leaf_objective); @@ -746,46 +824,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); std::vector root_vstatus; std::vector edge_norms; - settings.log.printf("Solving LP root relaxation\n"); - simplex_solver_settings_t lp_settings = settings; - lp_settings.inside_mip = 1; - lp_status_t root_status = solve_linear_program_advanced( - original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); - stats.total_lp_solve_time = toc(start_time); - assert(root_vstatus.size() == original_lp.num_cols); - if (root_status == lp_status_t::INFEASIBLE) { - settings.log.printf("MIP Infeasible\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::INFEASIBLE; - } - if (root_status == lp_status_t::UNBOUNDED) { - settings.log.printf("MIP Unbounded\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); - } - return mip_status_t::UNBOUNDED; - } - if (root_status == lp_status_t::TIME_LIMIT) { - settings.log.printf("Hit time limit\n"); - return mip_status_t::TIME_LIMIT; - } - set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); + f_t root_objective; + + mip_status_t root_status = + solve_root_relaxation(root_objective, root_relax_soln, root_vstatus, edge_norms, stats); + if (root_status != mip_status_t::UNSET) { return root_status; } std::vector fractional; const i_t num_fractional = fractional_variables(settings, root_relax_soln.x, var_types, fractional); - const f_t root_objective = compute_objective(original_lp, root_relax_soln.x); - if (settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); - settings.set_simplex_solution_callback(original_x, - compute_user_objective(original_lp, root_objective)); - } - global_variables::mutex_lower.lock(); - stats.lower_bound = global_variables::lower_bound = root_objective; - global_variables::mutex_lower.unlock(); if (num_fractional == 0) { global_variables::mutex_upper.lock(); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index a533fa7f55..1b9cf78e60 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -80,6 +81,7 @@ class branch_and_bound_t { struct stats_t { f_t lower_bound = 0.0; + f_t upper_bound = inf; f_t gap = 0.0; f_t total_lp_solve_time = 0.0; i_t nodes_explored = 0; @@ -111,6 +113,26 @@ class branch_and_bound_t { std::vector& parent_vstatus, stats_t& stats); + void add_feasible_solution(mip_node_t* leaf_ptr, + f_t leaf_objective, + const std::vector& leaf_sol, + mip_solution_t& incumbent, + stats_t& stats, + char symbol); + + mip_status_t solve_root_relaxation(f_t& root_objective, + lp_solution_t& root_relax_soln, + std::vector& root_vstatus, + std::vector& edge_norms, + stats_t& stats); + + dual::status_t solve_leaf_lp(mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + std::vector& leaf_vstatus, + lp_solution_t& leaf_solution, + std::vector& edge_norms, + stats_t& stats); + mip_status_t diving(f_t root_objective, std::vector& edge_norms, pseudo_costs_t pc, From 9fbb03064286362d2e8fd16b871bb41f965c7f4f Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 18:10:24 +0200 Subject: [PATCH 14/44] Added depth first search using the refactored code --- .../linear_programming/cuopt/run_mip.cpp | 45 +- .../mip/solver_settings.hpp | 12 +- cpp/src/dual_simplex/branch_and_bound.cpp | 456 +++++++++--------- cpp/src/dual_simplex/branch_and_bound.hpp | 51 +- cpp/src/dual_simplex/solve.cpp | 12 +- cpp/src/dual_simplex/solve.hpp | 4 +- cpp/src/mip/solver.cu | 2 +- cpp/tests/dual_simplex/unit_tests/solve.cpp | 8 +- 8 files changed, 292 insertions(+), 298 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index ead62c5168..672c5b3e5f 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -153,7 +153,7 @@ int run_single_file(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - bool diving) + cuopt::linear_programming::search_settings_t::strategy_t search_strategy) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -170,8 +170,7 @@ int run_single_file(std::string file_path, } } - settings.diving_settings.use_diving = diving; - + settings.search_settings.strategy = search_strategy; constexpr bool input_mps_strict = false; cuopt::mps_parser::mps_data_model_t mps_data_model; @@ -259,7 +258,7 @@ void run_single_file_mp(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - bool diving) + cuopt::linear_programming::search_settings_t::strategy_t search_strategy) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -274,7 +273,7 @@ void run_single_file_mp(std::string file_path, write_log_file, log_to_console, time_limit, - diving); + search_strategy); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes exit(sol_found); @@ -348,12 +347,12 @@ int main(int argc, char* argv[]) .scan<'g', double>() .default_value(std::numeric_limits::max()); - program.add_argument("--diving") - .help("enable diving (t/f)") - .default_value(std::string("f")); + program.add_argument("--search-strategy") + .help("Search strategy used in B&B (bfs/bfs-diving/dfs)") + .default_value(std::string("bfs")); program.add_argument("--gpu") - .help("id of the GPU to use (default: 0)") + .help("id of the GPU to use when running a single test (default: 0)") .scan<'i', int>() .default_value(0); @@ -379,12 +378,24 @@ int main(int argc, char* argv[]) std::string result_file; int batch_num; - bool heuristics_only = program.get("--heuristics-only")[0] == 't'; - int num_cpu_threads = program.get("--num-cpu-threads"); - bool write_log_file = program.get("--write-log-file")[0] == 't'; - bool log_to_console = program.get("--log-to-console")[0] == 't'; - bool diving = program.get("--diving")[0] == 't'; - int gpu_id = program.get("--gpu"); + bool heuristics_only = program.get("--heuristics-only")[0] == 't'; + int num_cpu_threads = program.get("--num-cpu-threads"); + bool write_log_file = program.get("--write-log-file")[0] == 't'; + bool log_to_console = program.get("--log-to-console")[0] == 't'; + std::string search_strategy_cli = program.get("--search-strategy"); + int gpu_id = program.get("--gpu"); + + cuopt::linear_programming::search_settings_t::strategy_t search_strategy; + if (search_strategy_cli == "bfs") { + search_strategy = cuopt::linear_programming::search_settings_t::strategy_t::BEST_FIRST; + } else if (search_strategy_cli == "bfs-diving") { + search_strategy = cuopt::linear_programming::search_settings_t::strategy_t::BEST_FIRST_DIVING; + } else if (search_strategy_cli == "dfs") { + search_strategy = cuopt::linear_programming::search_settings_t::strategy_t::DEPTH_FIRST; + } else { + std::cerr << "Invalid search strategy: " << search_strategy_cli << std::endl; + exit(1); + } if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); @@ -471,7 +482,7 @@ int main(int argc, char* argv[]) write_log_file, log_to_console, time_limit, - diving); + search_strategy); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; exit(1); @@ -504,7 +515,7 @@ int main(int argc, char* argv[]) write_log_file, log_to_console, time_limit, - diving); + search_strategy); } return 0; diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 9ef55a5ac9..691163b65a 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -35,8 +35,14 @@ struct benchmark_info_t { double objective_of_initial_population = std::numeric_limits::max(); }; -struct diving_settings_t { - bool use_diving = false; +struct search_settings_t { + enum class strategy_t { + BEST_FIRST = 0, + DEPTH_FIRST = 1, + BEST_FIRST_DIVING = 2, + }; + + strategy_t strategy = strategy_t::BEST_FIRST; }; // Forward declare solver_settings_t for friend class @@ -83,7 +89,7 @@ class mip_solver_settings_t { f_t relative_mip_gap = 1.0e-4; }; - diving_settings_t diving_settings; + search_settings_t search_settings; /** * @brief Get the tolerance settings as a single structure. diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 94ecbff18c..0aef168e02 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -405,10 +405,10 @@ template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, - const diving_settings_t strategy) + const search_settings_t strategy) : original_problem(user_problem), settings(solver_settings), - diving_settings(strategy), + search_settings(strategy), original_lp(1, 1, 1) { start_time = tic(); @@ -652,7 +652,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, f_t branch_var_val, std::vector& root_vstatus, std::vector& edge_norms, - pseudo_costs_t& pc, + pseudo_costs_t pc, mip_solution_t& incumbent, mip_solution_t& solution) { @@ -664,7 +664,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, b->lower_bound; // True if a comes before b, elements that come before are output last }; - stats.num_nodes = 0; + stats.num_nodes = 1; std::priority_queue*, std::vector*>, decltype(compare)> heap(compare); @@ -797,6 +797,162 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, stats.unexplored_nodes = heap.size(); } +template +void branch_and_bound_t::depth_first_solve(stats_t& stats, + f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t pc, + mip_solution_t& incumbent, + mip_solution_t& solution, + bool enable_reporting) +{ + logger_t log; + log.log = false; + + stats.num_nodes = 1; + std::vector*> node_stack; + + mip_node_t root_node(root_objective, root_vstatus); + graphviz_node(settings, &root_node, "lower bound", stats.lower_bound); + + branch(&root_node, branch_var, branch_var_val, root_vstatus, stats); + + // the stack does not own the unique_ptr the tree does + node_stack.push_back(root_node.get_down_child()); + node_stack.push_back(root_node.get_up_child()); + + // Make a copy of the original LP. We will modify its bounds at each leaf + lp_problem_t leaf_problem = original_lp; + + stats.gap = get_upper_bound() - stats.lower_bound; + stats.nodes_explored = 0; + stats.total_lp_iters = 0.0; + f_t last_log = 0; + while (stats.gap > settings.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), stats.lower_bound) > settings.relative_mip_gap_tol && + node_stack.size() > 0) { + repair_heuristic_solutions(root_vstatus, edge_norms, stats.lower_bound, incumbent, solution); + + // Get a node off the stack + mip_node_t* node_ptr = node_stack.back(); + node_stack.pop_back(); + + stats.upper_bound = get_upper_bound(); + stats.gap = stats.upper_bound - stats.lower_bound; + const i_t leaf_depth = node_ptr->depth; + f_t now = toc(start_time); + + if (enable_reporting) { + f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + if ((stats.nodes_explored % 1000 == 0 || stats.gap < 10 * settings.absolute_mip_gap_tol || + stats.nodes_explored < 1000) && + (time_since_log >= 1) || + (time_since_log > 60) || now > settings.time_limit) { + settings.log.printf( + " %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + stats.nodes_explored, + node_stack.size(), + compute_user_objective(original_lp, stats.upper_bound), + compute_user_objective(original_lp, stats.lower_bound), + leaf_depth, + stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, stats.upper_bound), + compute_user_objective(original_lp, stats.lower_bound)) + .c_str(), + now); + last_log = tic(); + } + } + + if (now > settings.time_limit) { + if (enable_reporting) { settings.log.printf("Hit time limit. Stoppping\n"); } + stats.status = mip_status_t::TIME_LIMIT; + break; + } + + std::vector& leaf_vstatus = node_ptr->vstatus; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + dual::status_t lp_status = + solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, stats); + + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { + node_ptr->lower_bound = inf; + std::vector*> stack; + node_ptr->set_status(node_status_t::INFEASIBLE, stack); + graphviz_node(settings, node_ptr, "infeasible", 0.0); + remove_fathomed_nodes(stack); + // Node was infeasible. Do not branch + } else if (lp_status == dual::status_t::CUTOFF) { + node_ptr->lower_bound = stats.upper_bound; + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "cut off", leaf_objective); + remove_fathomed_nodes(stack); + // Node was cut off. Do not branch + } else if (lp_status == dual::status_t::OPTIMAL) { + // LP was feasible + std::vector fractional; + const i_t leaf_fractional = + fractional_variables(settings, leaf_solution.x, var_types, fractional); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + + pc.update_pseudo_costs(node_ptr, leaf_objective); + node_ptr->lower_bound = leaf_objective; + + constexpr f_t fathom_tol = 1e-5; + if (leaf_fractional == 0) { + stats.unexplored_nodes = node_stack.size(); + add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, stats, 'D'); + + } else if (leaf_objective <= stats.upper_bound + fathom_tol) { + // Choose fractional variable to branch on + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + assert(leaf_vstatus.size() == leaf_problem.num_cols); + + branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, stats); + + // Martin's child selection + const f_t down_val = std::floor(root_node.fractional_val); + const f_t up_val = std::ceil(root_node.fractional_val); + const f_t down_dist = leaf_solution.x[branch_var] - down_val; + const f_t up_dist = up_val - leaf_solution.x[branch_var]; + + if (down_dist < up_dist) { + node_stack.push_back(node_ptr->get_up_child()); + node_stack.push_back(node_ptr->get_down_child()); + } else { + node_stack.push_back(node_ptr->get_down_child()); + node_stack.push_back(node_ptr->get_up_child()); + } + + } else { + graphviz_node(settings, node_ptr, "fathomed", leaf_objective); + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + remove_fathomed_nodes(stack); + } + } else { + graphviz_node(settings, node_ptr, "numerical", 0.0); + + if (enable_reporting) { + settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", + lp_status); + } + + stats.status = mip_status_t::NUMERICAL; + break; + } + } + + stats.unexplored_nodes = node_stack.size(); +} + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -875,40 +1031,70 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t branch_var = pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); + if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { + settings.log.printf("Using depth first search\n"); + } else if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { + settings.log.printf("Using best first search with diving\n"); + } else { + settings.log.printf("Using best first search\n"); + } + settings.log.printf( "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " " Time \n"); + global_variables::mutex_branching.lock(); global_variables::currently_branching = true; global_variables::mutex_branching.unlock(); - std::future diving_thread; - - if (diving_settings.use_diving) { - settings.log.printf("Launching the diving thread!\n"); - diving_thread = std::async(std::launch::async, [&]() { - return diving(root_objective, - edge_norms, - pc, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - incumbent); - }); + if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { + + depth_first_solve(stats, + root_objective, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + edge_norms, + pc, + incumbent, + solution, + true); + } else { + + std::future diving_thread; + + if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { + diving_thread = std::async(std::launch::async, [&]() { + return depth_first_solve(stats, + root_objective, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + edge_norms, + pc, + incumbent, + solution, + false); + }); + + } + + best_first_solve(stats, + root_objective, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + edge_norms, + pc, + incumbent, + solution); + + if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { + // TODO: Instead of waiting, we should send a signal that we are already done in the main thread + diving_thread.get(); + } } - best_first_solve(stats, - root_objective, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - edge_norms, - pc, - incumbent, - solution); - - if (diving_settings.use_diving) { mip_status_t diving_status = diving_thread.get(); } - global_variables::mutex_branching.lock(); global_variables::currently_branching = false; global_variables::mutex_branching.unlock(); @@ -962,218 +1148,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return stats.status; } -template -mip_status_t branch_and_bound_t::diving( - f_t root_objective, - std::vector& edge_norms, - pseudo_costs_t pc, - i_t branch_var, - f_t branch_var_val, - const std::vector& root_vstatus, - mip_solution_t& incumbent) -{ - logger_t log; - log.log = false; - - mip_node_t root_node(root_objective, root_vstatus); - - mip_status_t status = mip_status_t::UNSET; - f_t lower_bound = root_objective; - std::vector*> node_stack; - i_t num_nodes = 1; - - // down child - std::unique_ptr> down_child = std::make_unique>( - original_lp, &root_node, ++num_nodes, branch_var, 0, branch_var_val, root_vstatus); - - // up child - std::unique_ptr> up_child = std::make_unique>( - original_lp, &root_node, ++num_nodes, branch_var, 1, branch_var_val, root_vstatus); - - node_stack.push_back(down_child.get()); // the stack does not own the unique_ptr the tree does - node_stack.push_back(up_child.get()); // the stack does not own the unqiue_ptr the tree does - root_node.add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree - - // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp; - f_t gap = get_upper_bound() - lower_bound; - i_t nodes_explored = 0; - f_t total_lp_iters = 0.0; - f_t last_log = 0; - while (gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && - node_stack.size() > 0) { - // Get a node off the stack - mip_node_t* node_ptr = node_stack.back(); - node_stack.pop_back(); // Remove node from the stack - f_t upper_bound = get_upper_bound(); - - gap = upper_bound - lower_bound; - const i_t leaf_depth = node_ptr->depth; - f_t now = toc(start_time); - f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - - if (now > settings.time_limit) { - status = mip_status_t::TIME_LIMIT; - break; - } - - // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp.lower; - leaf_problem.upper = original_lp.upper; - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); - - std::vector& leaf_vstatus = node_ptr->vstatus; - lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); - - i_t node_iter = 0; - assert(leaf_vstatus.size() == leaf_problem.num_cols); - f_t lp_start_time = tic(); - std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; - - // Solve the LP relaxation of the current node using Dual Simplex and the previous basis - simplex_solver_settings_t lp_settings = settings; - lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings.dual_tol; - lp_settings.inside_mip = 2; - dual::status_t lp_status = dual_phase2(2, - 0, - lp_start_time, - leaf_problem, - lp_settings, - leaf_vstatus, - leaf_solution, - node_iter, - leaf_edge_norms); - if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d (diving). Resolving from scratch.\n", - nodes_explored); - lp_status_t second_status = solve_linear_program_advanced( - leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); - lp_status = convert_lp_status_to_dual_status(second_status); - } - total_lp_iters += node_iter; - - nodes_explored++; - if (lp_status == dual::status_t::DUAL_UNBOUNDED) { - node_ptr->lower_bound = inf; - std::vector*> stack; - node_ptr->set_status(node_status_t::INFEASIBLE, stack); - graphviz_node(settings, node_ptr, "infeasible", 0.0); - remove_fathomed_nodes(stack); - // Node was infeasible. Do not branch - - } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = upper_bound; - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "cut off", leaf_objective); - remove_fathomed_nodes(stack); - // Node was cut off. Do not branch - - } else if (lp_status == dual::status_t::OPTIMAL) { - // LP was feasible - std::vector fractional; - const i_t leaf_fractional = - fractional_variables(settings, leaf_solution.x, var_types, fractional); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "lower bound", leaf_objective); - - pc.update_pseudo_costs(node_ptr, leaf_objective); - - node_ptr->lower_bound = leaf_objective; - - constexpr f_t fathom_tol = 1e-5; - if (leaf_fractional == 0) { - bool send_solution = false; - global_variables::mutex_upper.lock(); - if (leaf_objective < global_variables::upper_bound) { - incumbent.set_incumbent_solution(leaf_objective, leaf_solution.x); - global_variables::upper_bound = upper_bound = leaf_objective; - gap = upper_bound - lower_bound; - settings.log.printf("D%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - node_stack.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), - leaf_depth, - nodes_explored > 0 ? total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - toc(start_time)); - send_solution = true; - } - - if (send_solution && settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); - settings.solution_callback(original_x, upper_bound); - } - global_variables::mutex_upper.unlock(); - - std::vector*> stack; - node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); - remove_fathomed_nodes(stack); - - } else if (leaf_objective <= upper_bound + fathom_tol) { - // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - assert(leaf_vstatus.size() == leaf_problem.num_cols); - - // down child - std::unique_ptr> down_child = - std::make_unique>(original_lp, - node_ptr, - ++num_nodes, - branch_var, - 0, - leaf_solution.x[branch_var], - leaf_vstatus); - - // up child - std::unique_ptr> up_child = - std::make_unique>(original_lp, - node_ptr, - ++num_nodes, - branch_var, - 1, - leaf_solution.x[branch_var], - leaf_vstatus); - - // Martin's child selection - const f_t down_val = std::floor(root_node.fractional_val); - const f_t up_val = std::ceil(root_node.fractional_val); - const f_t down_dist = leaf_solution.x[branch_var] - down_val; - const f_t up_dist = up_val - leaf_solution.x[branch_var]; - - if (down_dist < up_dist) { - node_stack.push_back(up_child.get()); - node_stack.push_back(down_child.get()); - } else { - node_stack.push_back(up_child.get()); - node_stack.push_back(down_child.get()); - } - - node_ptr->add_children(std::move(down_child), - std::move(up_child)); // child pointers moved into the tree - } else { - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - remove_fathomed_nodes(stack); - } - } else { - status = mip_status_t::NUMERICAL; - break; - } - } - - return status; -} - #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template class branch_and_bound_t; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 1b9cf78e60..29367a3205 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -18,12 +18,12 @@ #pragma once #include +#include #include #include #include #include #include -#include #include #include @@ -48,9 +48,21 @@ void upper_bound_callback(f_t upper_bound); template class branch_and_bound_t { public: + struct stats_t { + f_t lower_bound = 0.0; + f_t upper_bound = inf; + f_t gap = 0.0; + f_t total_lp_solve_time = 0.0; + i_t nodes_explored = 0; + i_t unexplored_nodes = 0; + f_t total_lp_iters = 0; + i_t num_nodes = 0; + mip_status_t status = mip_status_t::UNSET; + }; + branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, - const diving_settings_t strategy); + const search_settings_t strategy); // Set an initial guess based on the user_problem. This should be called before solve. void set_initial_guess(const std::vector& user_guess) { guess = user_guess; } @@ -70,7 +82,7 @@ class branch_and_bound_t { private: const user_problem_t& original_problem; const simplex_solver_settings_t settings; - diving_settings_t diving_settings; + search_settings_t search_settings; f_t start_time; std::vector guess; @@ -79,18 +91,6 @@ class branch_and_bound_t { std::vector new_slacks; std::vector var_types; - struct stats_t { - f_t lower_bound = 0.0; - f_t upper_bound = inf; - f_t gap = 0.0; - f_t total_lp_solve_time = 0.0; - i_t nodes_explored = 0; - i_t unexplored_nodes = 0; - f_t total_lp_iters = 0; - i_t num_nodes = 0; - mip_status_t status = mip_status_t::UNSET; - }; - void repair_heuristic_solutions(const std::vector& root_vstatus, const std::vector& edge_norms, const f_t& lower_bound, @@ -103,10 +103,21 @@ class branch_and_bound_t { f_t branch_var_val, std::vector& root_vstatus, std::vector& edge_norms, - pseudo_costs_t& pc, + pseudo_costs_t pc, mip_solution_t& incumbent, mip_solution_t& solution); + void depth_first_solve(stats_t& stats, + f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t pc, + mip_solution_t& incumbent, + mip_solution_t& solution, + bool enable_reporting); + void branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, @@ -132,14 +143,6 @@ class branch_and_bound_t { lp_solution_t& leaf_solution, std::vector& edge_norms, stats_t& stats); - - mip_status_t diving(f_t root_objective, - std::vector& edge_norms, - pseudo_costs_t pc, - i_t branch_var, - f_t branch_var_val, - const std::vector& root_vstatus, - mip_solution_t& incumbent); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 77ee589834..a318b42a3d 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -266,12 +266,12 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, template i_t solve(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const diving_settings_t diving_settings, + const search_settings_t search_settings, std::vector& primal_solution) { i_t status; if (is_mip(problem) && !settings.relaxation) { - branch_and_bound_t branch_and_bound(problem, settings, diving_settings); + branch_and_bound_t branch_and_bound(problem, settings, search_settings); mip_solution_t mip_solution(problem.num_cols); mip_status_t mip_status = branch_and_bound.solve(mip_solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -304,13 +304,13 @@ i_t solve(const user_problem_t& problem, template i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const diving_settings_t diving_settings, + const search_settings_t search_settings, const std::vector& guess, mip_solution_t& solution) { i_t status; if (is_mip(problem)) { - branch_and_bound_t branch_and_bound(problem, settings, diving_settings); + branch_and_bound_t branch_and_bound(problem, settings, search_settings); branch_and_bound.set_initial_guess(guess); mip_status_t mip_status = branch_and_bound.solve(solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -351,13 +351,13 @@ template lp_status_t solve_linear_program(const user_problem_t& use template int solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, - const diving_settings_t diving_settings, + const search_settings_t search_settings, std::vector& primal_solution); template int solve_mip_with_guess( const user_problem_t& problem, const simplex_solver_settings_t& settings, - const diving_settings_t diving_settings, + const search_settings_t search_settings, const std::vector& guess, mip_solution_t& solution); diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index fce026b8ce..6f51a0923f 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -68,14 +68,14 @@ i_t solve_mip(const user_problem_t& user_problem, mip_solution_t i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const diving_settings_t diving_settings, + const search_settings_t search_settings, const std::vector& guess, mip_solution_t& solution); template i_t solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, - const diving_settings_t diving_settings, + const search_settings_t search_settings, std::vector& primal_solution); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index f0bb65de36..94edeb5ae9 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -190,7 +190,7 @@ solution_t mip_solver_t::run_solver() // Create the branch and bound object branch_and_bound = std::make_unique>( - branch_and_bound_problem, branch_and_bound_settings, context.settings.diving_settings); + branch_and_bound_problem, branch_and_bound_settings, context.settings.search_settings); // Set the primal heuristics -> branch and bound callback context.problem_ptr->branch_and_bound_callback = diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 617d29ae64..23ff25bdb5 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -88,7 +88,7 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::CONTINUOUS; user_problem.var_types[1] = dual_simplex::variable_type_t::CONTINUOUS; - diving_settings_t diving_settings; + search_settings_t search_settings; double start_time = dual_simplex::tic(); dual_simplex::simplex_solver_settings_t settings; @@ -103,7 +103,7 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::INTEGER; user_problem.var_types[1] = dual_simplex::variable_type_t::INTEGER; - EXPECT_EQ((dual_simplex::solve(user_problem, settings, diving_settings, solution.x)), 0); + EXPECT_EQ((dual_simplex::solve(user_problem, settings, search_settings, solution.x)), 0); } TEST(dual_simplex, burglar) @@ -164,12 +164,12 @@ TEST(dual_simplex, burglar) user_problem.var_types[j] = cuopt::linear_programming::dual_simplex::variable_type_t::INTEGER; } - diving_settings_t diving_settings; + search_settings_t search_settings; cuopt::linear_programming::dual_simplex::simplex_solver_settings_t settings; std::vector solution(num_items); EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve( - user_problem, settings, diving_settings, solution)), + user_problem, settings, search_settings, solution)), 0); double objective = 0.0; for (int j = 0; j < num_items; ++j) { From 4fd9cf59831e51ad8290ec12cd2d4ec0035e7688 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 22 Aug 2025 18:26:44 +0200 Subject: [PATCH 15/44] Fixed hanging due to incorrect calculation of the lower bounds for an empty stack. --- cpp/src/dual_simplex/branch_and_bound.cpp | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 0aef168e02..17055cea37 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -656,6 +656,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, mip_solution_t& incumbent, mip_solution_t& solution) { + stats.status = mip_status_t::UNSET; logger_t log; log.log = false; @@ -795,6 +796,13 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, } stats.unexplored_nodes = heap.size(); + + if (stats.unexplored_nodes == 0) { + global_variables::mutex_lower.lock(); + stats.lower_bound = global_variables::lower_bound = root_node.lower_bound; + global_variables::mutex_lower.unlock(); + stats.gap = get_upper_bound() - stats.lower_bound; + } } template @@ -809,6 +817,8 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, mip_solution_t& solution, bool enable_reporting) { + stats.status = mip_status_t::UNSET; + logger_t log; log.log = false; @@ -951,6 +961,13 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, } stats.unexplored_nodes = node_stack.size(); + + if (stats.unexplored_nodes == 0) { + global_variables::mutex_lower.lock(); + stats.lower_bound = global_variables::lower_bound = root_node.lower_bound; + global_variables::mutex_lower.unlock(); + stats.gap = get_upper_bound() - stats.lower_bound; + } } template @@ -1099,12 +1116,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut global_variables::currently_branching = false; global_variables::mutex_branching.unlock(); - if (stats.unexplored_nodes == 0) { - global_variables::mutex_lower.lock(); - stats.lower_bound = global_variables::lower_bound = root_objective; - global_variables::mutex_lower.unlock(); - stats.gap = get_upper_bound() - stats.lower_bound; - } settings.log.printf( "Explored %d nodes in %.2fs.\nAbsolute Gap %e Objective %.16e Lower Bound %.16e\n", From 876d897aabf01282c4de4592b0dee837cdbdcbad Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 25 Aug 2025 14:28:51 +0200 Subject: [PATCH 16/44] Fixed bugs caused by merge --- cpp/src/dual_simplex/branch_and_bound.cpp | 10 ++++++---- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b6e7aa4189..bb09827f22 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include "cuopt/linear_programming/mip/solver_settings.hpp" @@ -210,6 +211,8 @@ f_t relative_gap(f_t obj_value, f_t lower_bound) f_t user_mip_gap = obj_value == 0.0 ? (lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) : std::abs(obj_value - lower_bound) / std::abs(obj_value); + // Handle NaNs (i.e., NaN != NaN) + if (user_mip_gap != user_mip_gap) { return std::numeric_limits::infinity(); } return user_mip_gap; } @@ -637,6 +640,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, stats.nodes_explored = 0; stats.total_lp_iters = 0.0; f_t last_log = 0; + while (stats.gap > settings.absolute_mip_gap_tol && relative_gap(get_upper_bound(), stats.lower_bound) > settings.relative_mip_gap_tol && heap.size() > 0) { @@ -727,7 +731,6 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, stats, 'B'); } else if (leaf_objective <= stats.upper_bound + fathom_tol) { - mutex_pseudocosts.lock(); // Choose fractional variable to branch on const i_t branch_var = pc.variable_selection( @@ -927,9 +930,8 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, break; } } - mutex_branching.lock(); - currently_branching = false; - mutex_branching.unlock(); + + stats.unexplored_nodes = node_stack.size(); if (stats.unexplored_nodes == 0) { mutex_lower.lock(); diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 381f5b1258..401433b385 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -17,6 +17,7 @@ #pragma once +#include "cuopt/linear_programming/mip/solver_settings.hpp" #include "recombiner.cuh" #include @@ -104,10 +105,14 @@ class sub_mip_recombiner_t : public recombiner_t { f_t objective) { this->solution_callback(solution, objective); }; + + search_settings_t search_settings; + // disable B&B logs, so that it is not interfering with the main B&B thread branch_and_bound_settings.log.log = false; dual_simplex::branch_and_bound_t branch_and_bound(branch_and_bound_problem, - branch_and_bound_settings); + branch_and_bound_settings, + search_settings); branch_and_bound_status = branch_and_bound.solve(branch_and_bound_solution); if (solution_vector.size() > 0) { cuopt_assert(fixed_assignment.size() == branch_and_bound_solution.x.size(), From cf4426f34c0e15e243e55b3676ef1b44a0cfd399 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 25 Aug 2025 15:16:26 +0200 Subject: [PATCH 17/44] merged stats structs --- cpp/src/dual_simplex/branch_and_bound.cpp | 389 ++++++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 88 +++-- 2 files changed, 248 insertions(+), 229 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index bb09827f22..df2394619e 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -199,6 +199,15 @@ f_t branch_and_bound_t::get_upper_bound() return upper_bound; } +template +f_t branch_and_bound_t::get_lower_bound() +{ + mutex_lower.lock(); + const f_t lower_bound = lower_bound_; + mutex_lower.unlock(); + return lower_bound; +} + template f_t sgn(f_t x) { @@ -282,11 +291,11 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu user_mip_gap(compute_user_objective(original_lp, obj), compute_user_objective(original_lp, lower_bound)) .c_str(), - toc(start_time)); + toc(stats_.start_time)); } else { settings.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", compute_user_objective(original_lp, obj), - toc(start_time)); + toc(stats_.start_time)); } } @@ -368,7 +377,7 @@ branch_and_bound_t::branch_and_bound_t( original_lp(1, 1, 1), incumbent(1) { - start_time = tic(); + stats_.start_time = tic(); convert_user_problem(original_problem, settings, original_lp, new_slacks); full_variable_types(original_problem, original_lp, var_types); @@ -425,7 +434,7 @@ void branch_and_bound_t::repair_heuristic_solutions( obj, lower, gap.c_str(), - toc(start_time)); + toc(stats_.start_time)); if (settings.solution_callback != nullptr) { std::vector original_x; @@ -444,18 +453,17 @@ template void branch_and_bound_t::branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, - std::vector& parent_vstatus, - stats_t& stats) + std::vector& parent_vstatus) { // down child std::unique_ptr> down_child = std::make_unique>( - original_lp, parent_node, ++stats.num_nodes, branch_var, 0, branch_var_val, parent_vstatus); + original_lp, parent_node, ++stats_.num_nodes, branch_var, 0, branch_var_val, parent_vstatus); graphviz_edge(settings, parent_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); // up child std::unique_ptr> up_child = std::make_unique>( - original_lp, parent_node, ++stats.num_nodes, branch_var, 1, branch_var_val, parent_vstatus); + original_lp, parent_node, ++stats_.num_nodes, branch_var, 1, branch_var_val, parent_vstatus); graphviz_edge(settings, parent_node, up_child.get(), branch_var, 1, std::ceil(branch_var_val)); @@ -469,34 +477,38 @@ void branch_and_bound_t::add_feasible_solution(mip_node_t* l f_t leaf_objective, const std::vector& leaf_sol, mip_solution_t& incumbent, - stats_t& stats, + i_t nodes_explored, + i_t unexplored_nodes, char symbol) { bool send_solution = false; + f_t lower_bound = get_lower_bound(); + mutex_upper.lock(); if (leaf_objective < upper_bound_) { incumbent.set_incumbent_solution(leaf_objective, leaf_sol); - upper_bound_ = stats.upper_bound = leaf_objective; - stats.gap = stats.upper_bound - stats.lower_bound; - f_t obj = compute_user_objective(original_lp, stats.upper_bound); - f_t lower = compute_user_objective(original_lp, stats.lower_bound); - settings.log.printf("%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - symbol, - stats.nodes_explored, - stats.unexplored_nodes, - obj, - lower, - leaf_ptr->depth, - stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(start_time)); + upper_bound_ = leaf_objective; + gap_ = upper_bound_ - lower_bound; + f_t obj = compute_user_objective(original_lp, upper_bound_); + f_t lower = compute_user_objective(original_lp, lower_bound); + settings.log.printf( + "%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + symbol, + nodes_explored, + unexplored_nodes, + obj, + lower, + leaf_ptr->depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(stats_.start_time)); send_solution = true; } if (send_solution && settings.solution_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); - settings.solution_callback(original_x, stats.upper_bound); + settings.solution_callback(original_x, upper_bound_); } mutex_upper.unlock(); @@ -512,15 +524,14 @@ mip_status_t branch_and_bound_t::solve_root_relaxation( f_t& root_objective, lp_solution_t& root_relax_soln, std::vector& root_vstatus, - std::vector& edge_norms, - stats_t& stats) + std::vector& edge_norms) { settings.log.printf("Solving LP root relaxation\n"); simplex_solver_settings_t lp_settings = settings; lp_settings.inside_mip = 1; lp_status_t root_status = solve_linear_program_advanced( - original_lp, start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); - stats.total_lp_solve_time = toc(start_time); + original_lp, stats_.start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); + stats_.total_lp_solve_time = toc(stats_.start_time); assert(root_vstatus.size() == original_lp.num_cols); if (root_status == lp_status_t::INFEASIBLE) { settings.log.printf("MIP Infeasible\n"); @@ -552,7 +563,7 @@ mip_status_t branch_and_bound_t::solve_root_relaxation( compute_user_objective(original_lp, root_objective)); } mutex_lower.lock(); - stats.lower_bound = lower_bound_ = root_objective; + lower_bound_ = root_objective; mutex_lower.unlock(); return mip_status_t::UNSET; @@ -565,7 +576,7 @@ dual::status_t branch_and_bound_t::solve_leaf_lp( std::vector& leaf_vstatus, lp_solution_t& leaf_solution, std::vector& edge_norms, - stats_t& stats) + f_t upper_bound) { // Set the correct bounds for the leaf problem leaf_problem.lower = original_lp.lower; @@ -578,7 +589,7 @@ dual::status_t branch_and_bound_t::solve_leaf_lp( std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; simplex_solver_settings_t lp_settings = settings; lp_settings.set_log(false); - lp_settings.cut_off = stats.upper_bound + settings.dual_tol; + lp_settings.cut_off = upper_bound + settings.dual_tol; lp_settings.inside_mip = 2; dual::status_t lp_status = dual_phase2(2, 0, @@ -590,30 +601,33 @@ dual::status_t branch_and_bound_t::solve_leaf_lp( node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", stats.nodes_explored); + settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", + stats_.nodes_explored.load()); lp_status_t second_status = solve_linear_program_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } - stats.total_lp_solve_time += toc(lp_start_time); - stats.total_lp_iters += node_iter; - stats.nodes_explored++; + + mutex_stats.lock(); + stats_.total_lp_solve_time += toc(lp_start_time); + stats_.total_lp_iters += node_iter; + mutex_stats.unlock(); return lp_status; } template -void branch_and_bound_t::best_first_solve(stats_t& stats, - f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, - mip_solution_t& solution) +mip_status_t branch_and_bound_t::best_first_solve( + f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t& pc, + mip_solution_t& incumbent, + mip_solution_t& solution) { - stats.status = mip_status_t::UNSET; + mip_status_t status = mip_status_t::UNSET; logger_t log; log.log = false; @@ -622,35 +636,34 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, b->lower_bound; // True if a comes before b, elements that come before are output last }; - stats.num_nodes = 1; std::priority_queue*, std::vector*>, decltype(compare)> heap(compare); mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", stats.lower_bound); + graphviz_node(settings, &root_node, "lower bound", lower_bound_); - branch(&root_node, branch_var, branch_var_val, root_vstatus, stats); + branch(&root_node, branch_var, branch_var_val, root_vstatus); heap.push(root_node.get_down_child()); // the heap does not own the unique_ptr the tree does heap.push(root_node.get_up_child()); // the heap does not own the unqiue_ptr the tree does // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp; - stats.gap = get_upper_bound() - stats.lower_bound; - stats.nodes_explored = 0; - stats.total_lp_iters = 0.0; - f_t last_log = 0; + f_t lower_bound = get_lower_bound(); + f_t gap = get_upper_bound() - lower_bound; + f_t last_log = 0; + i_t nodes_explored = 0; - while (stats.gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), stats.lower_bound) > settings.relative_mip_gap_tol && + while (gap > settings.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && heap.size() > 0) { - repair_heuristic_solutions(root_vstatus, edge_norms, stats.lower_bound, incumbent, solution); + repair_heuristic_solutions(root_vstatus, edge_norms, lower_bound, incumbent, solution); // Get a node off the heap mip_node_t* node_ptr = heap.top(); heap.pop(); // Remove node from the heap - stats.upper_bound = get_upper_bound(); - if (stats.upper_bound < node_ptr->lower_bound) { + f_t upper_bound = get_upper_bound(); + if (upper_bound < node_ptr->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the current // upper bound std::vector*> stack; @@ -660,41 +673,42 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, continue; } mutex_lower.lock(); - lower_bound_ = stats.lower_bound = node_ptr->lower_bound; + lower_bound_ = node_ptr->lower_bound; mutex_lower.unlock(); - stats.gap = stats.upper_bound - stats.lower_bound; + gap = upper_bound - lower_bound; const i_t leaf_depth = node_ptr->depth; - f_t now = toc(start_time); + f_t now = toc(stats_.start_time); f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((stats.nodes_explored % 1000 == 0 || stats.gap < 10 * settings.absolute_mip_gap_tol || - stats.nodes_explored < 1000) && + if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || + nodes_explored < 1000) && (time_since_log >= 1) || (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf( - " %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - stats.nodes_explored, - heap.size(), - compute_user_objective(original_lp, stats.upper_bound), - compute_user_objective(original_lp, stats.lower_bound), - leaf_depth, - stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, stats.upper_bound), - compute_user_objective(original_lp, stats.lower_bound)) - .c_str(), - now); + settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + heap.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound), + leaf_depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + now); last_log = tic(); } if (now > settings.time_limit) { settings.log.printf("Hit time limit. Stoppping\n"); - stats.status = mip_status_t::TIME_LIMIT; + status = mip_status_t::TIME_LIMIT; break; } std::vector& leaf_vstatus = node_ptr->vstatus; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); dual::status_t lp_status = - solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, stats); + solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, upper_bound); + + nodes_explored++; if (lp_status == dual::status_t::DUAL_UNBOUNDED) { node_ptr->lower_bound = inf; @@ -704,7 +718,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, remove_fathomed_nodes(stack); // Node was infeasible. Do not branch } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = stats.upper_bound; + node_ptr->lower_bound = upper_bound; std::vector*> stack; node_ptr->set_status(node_status_t::FATHOMED, stack); f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); @@ -727,10 +741,10 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { - stats.unexplored_nodes = heap.size(); - add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, stats, 'B'); + add_feasible_solution( + node_ptr, leaf_objective, leaf_solution.x, incumbent, nodes_explored, heap.size(), 'B'); - } else if (leaf_objective <= stats.upper_bound + fathom_tol) { + } else if (leaf_objective <= upper_bound + fathom_tol) { mutex_pseudocosts.lock(); // Choose fractional variable to branch on const i_t branch_var = pc.variable_selection( @@ -739,7 +753,7 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, assert(leaf_vstatus.size() == leaf_problem.num_cols); - branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, stats); + branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); heap.push( node_ptr->get_down_child()); // the heap does not own the unique_ptr the tree does heap.push(node_ptr->get_up_child()); // the heap does not own the unique_ptr the tree does @@ -754,45 +768,50 @@ void branch_and_bound_t::best_first_solve(stats_t& stats, graphviz_node(settings, node_ptr, "numerical", 0.0); settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", lp_status); - stats.status = mip_status_t::NUMERICAL; + status = mip_status_t::NUMERICAL; break; } } - stats.unexplored_nodes = heap.size(); + stats_.unexplored_nodes = heap.size(); + stats_.nodes_explored = nodes_explored; - if (stats.unexplored_nodes == 0) { + if (stats_.unexplored_nodes == 0) { mutex_lower.lock(); - stats.lower_bound = lower_bound_ = root_node.lower_bound; + lower_bound = lower_bound_ = root_node.lower_bound; mutex_lower.unlock(); - stats.gap = get_upper_bound() - stats.lower_bound; + + mutex_gap.lock(); + gap_ = gap = get_upper_bound() - lower_bound; + mutex_gap.unlock(); } + + return status; } template -void branch_and_bound_t::depth_first_solve(stats_t& stats, - f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, - mip_solution_t& solution, - bool enable_reporting) +mip_status_t branch_and_bound_t::depth_first_solve( + f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t& pc, + mip_solution_t& incumbent, + mip_solution_t& solution, + bool enable_reporting) { - stats.status = mip_status_t::UNSET; + mip_status_t status = mip_status_t::UNSET; logger_t log; log.log = false; - stats.num_nodes = 1; std::vector*> node_stack; mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", stats.lower_bound); + graphviz_node(settings, &root_node, "lower bound", root_objective); - branch(&root_node, branch_var, branch_var_val, root_vstatus, stats); + branch(&root_node, branch_var, branch_var_val, root_vstatus); // the stack does not own the unique_ptr the tree does node_stack.push_back(root_node.get_down_child()); @@ -801,56 +820,59 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp; - stats.gap = get_upper_bound() - stats.lower_bound; - stats.nodes_explored = 0; - stats.total_lp_iters = 0.0; - f_t last_log = 0; - while (stats.gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), stats.lower_bound) > settings.relative_mip_gap_tol && + f_t lower_bound = get_lower_bound(); + f_t gap = get_upper_bound() - lower_bound; + f_t last_log = 0; + i_t nodes_explored = 0; + + while (gap > settings.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && node_stack.size() > 0) { - repair_heuristic_solutions(root_vstatus, edge_norms, stats.lower_bound, incumbent, solution); + repair_heuristic_solutions(root_vstatus, edge_norms, lower_bound, incumbent, solution); // Get a node off the stack mip_node_t* node_ptr = node_stack.back(); node_stack.pop_back(); - stats.upper_bound = get_upper_bound(); - stats.gap = stats.upper_bound - stats.lower_bound; + f_t upper_bound = get_upper_bound(); + lower_bound = get_lower_bound(); + gap = upper_bound - lower_bound; const i_t leaf_depth = node_ptr->depth; - f_t now = toc(start_time); + f_t now = toc(stats_.start_time); if (enable_reporting) { f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((stats.nodes_explored % 1000 == 0 || stats.gap < 10 * settings.absolute_mip_gap_tol || - stats.nodes_explored < 1000) && + if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || + nodes_explored < 1000) && (time_since_log >= 1) || (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf( - " %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - stats.nodes_explored, - node_stack.size(), - compute_user_objective(original_lp, stats.upper_bound), - compute_user_objective(original_lp, stats.lower_bound), - leaf_depth, - stats.nodes_explored > 0 ? stats.total_lp_iters / stats.nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, stats.upper_bound), - compute_user_objective(original_lp, stats.lower_bound)) - .c_str(), - now); + settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + node_stack.size(), + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound), + leaf_depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + now); last_log = tic(); } } if (now > settings.time_limit) { if (enable_reporting) { settings.log.printf("Hit time limit. Stoppping\n"); } - stats.status = mip_status_t::TIME_LIMIT; + status = mip_status_t::TIME_LIMIT; break; } std::vector& leaf_vstatus = node_ptr->vstatus; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); dual::status_t lp_status = - solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, stats); + solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, upper_bound); + + nodes_explored++; if (lp_status == dual::status_t::DUAL_UNBOUNDED) { node_ptr->lower_bound = inf; @@ -860,7 +882,7 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, remove_fathomed_nodes(stack); // Node was infeasible. Do not branch } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = stats.upper_bound; + node_ptr->lower_bound = upper_bound; std::vector*> stack; node_ptr->set_status(node_status_t::FATHOMED, stack); f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); @@ -883,10 +905,9 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { - stats.unexplored_nodes = node_stack.size(); - add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, stats, 'D'); + add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, nodes_explored, node_stack.size(), 'D'); - } else if (leaf_objective <= stats.upper_bound + fathom_tol) { + } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on mutex_pseudocosts.lock(); @@ -896,7 +917,7 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, assert(leaf_vstatus.size() == leaf_problem.num_cols); - branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus, stats); + branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); // Martin's child selection const f_t down_val = std::floor(root_node.fractional_val); @@ -926,26 +947,32 @@ void branch_and_bound_t::depth_first_solve(stats_t& stats, lp_status); } - stats.status = mip_status_t::NUMERICAL; + status = mip_status_t::NUMERICAL; break; } } - stats.unexplored_nodes = node_stack.size(); + stats_.unexplored_nodes = node_stack.size(); + stats_.nodes_explored = nodes_explored; - if (stats.unexplored_nodes == 0) { + if (stats_.unexplored_nodes == 0) { mutex_lower.lock(); - stats.lower_bound = lower_bound_ = root_node.lower_bound; + lower_bound = lower_bound_ = root_node.lower_bound; mutex_lower.unlock(); - stats.gap = get_upper_bound() - lower_bound_; + gap = get_upper_bound() - lower_bound; + + mutex_gap.lock(); + gap_ = gap; + mutex_gap.unlock(); } + + return status; } template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { - stats_t stats; - stats.status = mip_status_t::UNSET; + mip_status_t status = mip_status_t::UNSET; mip_solution_t incumbent(original_lp.num_cols); if (guess.size() != 0) { @@ -971,7 +998,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t root_objective; mip_status_t root_status = - solve_root_relaxation(root_objective, root_relax_soln, root_vstatus, edge_norms, stats); + solve_root_relaxation(root_objective, root_relax_soln, root_vstatus, edge_norms); if (root_status != mip_status_t::UNSET) { return root_status; } std::vector fractional; @@ -986,12 +1013,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // We should be done here uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); solution.objective = incumbent.objective; - solution.lower_bound = stats.lower_bound; + solution.lower_bound = lower_bound_; solution.nodes_explored = 0; solution.simplex_iterations = root_relax_soln.iterations; settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", compute_user_objective(original_lp, root_objective), - toc(start_time)); + toc(stats_.start_time)); if (settings.set_simplex_solution_callback != nullptr) { settings.set_simplex_solution_callback(solution.x, solution.objective); } @@ -1004,7 +1031,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pseudo_costs_t pc(original_lp.num_cols); strong_branching(original_lp, settings, - start_time, + stats_.start_time, var_types, root_relax_soln.x, fractional, @@ -1019,6 +1046,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t branch_var = pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); + stats_.total_lp_iters = 0; + stats_.nodes_explored = 0; + stats_.unexplored_nodes = 0; + stats_.num_nodes = 1; + if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { settings.log.printf("Using depth first search\n"); } else if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { @@ -1036,23 +1068,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut mutex_branching.unlock(); if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { - depth_first_solve(stats, - root_objective, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - edge_norms, - pc, - incumbent, - solution, - true); + status = depth_first_solve(root_objective, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + edge_norms, + pc, + incumbent, + solution, + true); } else { - std::future diving_thread; + std::future diving_thread; if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { diving_thread = std::async(std::launch::async, [&]() { - return depth_first_solve(stats, - root_objective, + return depth_first_solve(root_objective, branch_var, root_relax_soln.x[branch_var], root_vstatus, @@ -1064,15 +1094,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut }); } - best_first_solve(stats, - root_objective, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - edge_norms, - pc, - incumbent, - solution); + status = best_first_solve(root_objective, + branch_var, + root_relax_soln.x[branch_var], + root_vstatus, + edge_norms, + pc, + incumbent, + solution); if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { // TODO: Instead of waiting, we should send a signal that we are already done in the main @@ -1087,20 +1116,20 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings.log.printf( "Explored %d nodes in %.2fs.\nAbsolute Gap %e Objective %.16e Lower Bound %.16e\n", - stats.nodes_explored, - toc(start_time), - stats.gap, + stats_.nodes_explored.load(), + toc(stats_.start_time), + gap_, compute_user_objective(original_lp, get_upper_bound()), - compute_user_objective(original_lp, stats.lower_bound)); + compute_user_objective(original_lp, lower_bound_)); - if (stats.gap <= settings.absolute_mip_gap_tol || - relative_gap(get_upper_bound(), stats.lower_bound) <= settings.relative_mip_gap_tol) { - stats.status = mip_status_t::OPTIMAL; - if (stats.gap > 0 && stats.gap <= settings.absolute_mip_gap_tol) { + if (gap_ <= settings.absolute_mip_gap_tol || + relative_gap(get_upper_bound(), lower_bound_) <= settings.relative_mip_gap_tol) { + status = mip_status_t::OPTIMAL; + if (gap_ > 0 && gap_ <= settings.absolute_mip_gap_tol) { settings.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", settings.absolute_mip_gap_tol); - } else if (stats.gap > 0 && relative_gap(get_upper_bound(), stats.lower_bound) <= - settings.relative_mip_gap_tol) { + } else if (gap_ > 0 && + relative_gap(get_upper_bound(), lower_bound_) <= settings.relative_mip_gap_tol) { settings.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", settings.relative_mip_gap_tol); } else { @@ -1111,9 +1140,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - if (stats.unexplored_nodes == 0 && get_upper_bound() == inf) { + if (stats_.unexplored_nodes == 0 && get_upper_bound() == inf) { settings.log.printf("Integer infeasible.\n"); - stats.status = mip_status_t::INFEASIBLE; + status = mip_status_t::INFEASIBLE; if (settings.heuristic_preemption_callback != nullptr) { settings.heuristic_preemption_callback(); } @@ -1121,10 +1150,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); solution.objective = incumbent.objective; - solution.lower_bound = stats.lower_bound; - solution.nodes_explored = stats.nodes_explored; - solution.simplex_iterations = stats.total_lp_iters; - return stats.status; + solution.lower_bound = get_lower_bound(); + solution.nodes_explored = stats_.nodes_explored; + solution.simplex_iterations = stats_.total_lp_iters; + return status; } #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 9bcab72a50..55266e8140 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -26,8 +26,8 @@ #include #include +#include #include -#include #include #include #include "cuopt/linear_programming/mip/solver_settings.hpp" @@ -50,18 +50,6 @@ void upper_bound_callback(f_t upper_bound); template class branch_and_bound_t { public: - struct stats_t { - f_t lower_bound = 0.0; - f_t upper_bound = inf; - f_t gap = 0.0; - f_t total_lp_solve_time = 0.0; - i_t nodes_explored = 0; - i_t unexplored_nodes = 0; - f_t total_lp_iters = 0; - i_t num_nodes = 0; - mip_status_t status = mip_status_t::UNSET; - }; - branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, const search_settings_t strategy); @@ -79,6 +67,8 @@ class branch_and_bound_t { std::vector& repaired_solution) const; f_t get_upper_bound(); + + f_t get_lower_bound(); // The main entry routine. Returns the solver status and populates solution with the incumbent. mip_status_t solve(mip_solution_t& solution); @@ -88,7 +78,6 @@ class branch_and_bound_t { const simplex_solver_settings_t settings; search_settings_t search_settings; - f_t start_time; std::vector guess; lp_problem_t original_lp; @@ -109,20 +98,24 @@ class branch_and_bound_t { // Mutex for gap std::mutex mutex_gap; // Global variable for gap - f_t gap; + f_t gap_; // Mutex for branching std::mutex mutex_branching; bool currently_branching; - // // Mutex for stats - // std::mutex mutex_stats; - // // Global variable for stats - // struct stats_t { - // int nodes_explored; - // f_t total_lp_solve_time; - // f_t start_time; - // } stats; + // Global variable for stats + std::mutex mutex_stats; + + // Note that floating point atomics are only supported in C++20 + struct stats_t { + f_t start_time = 0.0; + f_t total_lp_solve_time = 0.0; + std::atomic nodes_explored = 0; + std::atomic unexplored_nodes = 0; + f_t total_lp_iters = 0; + std::atomic num_nodes = 0; + } stats_; // Mutex for repair std::mutex mutex_repair; @@ -137,52 +130,49 @@ class branch_and_bound_t { mip_solution_t& incumbent, mip_solution_t& solution); - void best_first_solve(stats_t& stats, - f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, - mip_solution_t& solution); - - void depth_first_solve(stats_t& stats, - f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, - mip_solution_t& solution, - bool enable_reporting); + mip_status_t best_first_solve(f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t& pc, + mip_solution_t& incumbent, + mip_solution_t& solution); + + mip_status_t depth_first_solve(f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + std::vector& edge_norms, + pseudo_costs_t& pc, + mip_solution_t& incumbent, + mip_solution_t& solution, + bool enable_reporting); void branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, - std::vector& parent_vstatus, - stats_t& stats); + std::vector& parent_vstatus); void add_feasible_solution(mip_node_t* leaf_ptr, f_t leaf_objective, const std::vector& leaf_sol, mip_solution_t& incumbent, - stats_t& stats, + i_t nodes_explored, + i_t unexplored_nodes, char symbol); mip_status_t solve_root_relaxation(f_t& root_objective, lp_solution_t& root_relax_soln, std::vector& root_vstatus, - std::vector& edge_norms, - stats_t& stats); + std::vector& edge_norms); dual::status_t solve_leaf_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, std::vector& leaf_vstatus, lp_solution_t& leaf_solution, std::vector& edge_norms, - stats_t& stats); + f_t upper_bound); }; } // namespace cuopt::linear_programming::dual_simplex From df67a2b96dd46d6261a7b1716ca304cc9f6f3aa1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 25 Aug 2025 18:40:37 +0200 Subject: [PATCH 18/44] set common variables as class attributes. --- cpp/src/dual_simplex/branch_and_bound.cpp | 110 ++++++++-------------- cpp/src/dual_simplex/branch_and_bound.hpp | 44 ++++----- cpp/src/dual_simplex/pseudo_costs.hpp | 15 ++- 3 files changed, 75 insertions(+), 94 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index df2394619e..aaa0015f98 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -395,12 +395,8 @@ branch_and_bound_t::branch_and_bound_t( } template -void branch_and_bound_t::repair_heuristic_solutions( - const std::vector& root_vstatus, - const std::vector& edge_norms, - const f_t& lower_bound, - mip_solution_t& incumbent, - mip_solution_t& solution) +void branch_and_bound_t::repair_heuristic_solutions(const f_t& lower_bound, + mip_solution_t& solution) { // Check if there are any solutions to repair std::vector> to_repair; @@ -453,7 +449,7 @@ template void branch_and_bound_t::branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, - std::vector& parent_vstatus) + const std::vector& parent_vstatus) { // down child std::unique_ptr> down_child = std::make_unique>( @@ -476,7 +472,6 @@ template void branch_and_bound_t::add_feasible_solution(mip_node_t* leaf_ptr, f_t leaf_objective, const std::vector& leaf_sol, - mip_solution_t& incumbent, i_t nodes_explored, i_t unexplored_nodes, char symbol) @@ -491,17 +486,16 @@ void branch_and_bound_t::add_feasible_solution(mip_node_t* l gap_ = upper_bound_ - lower_bound; f_t obj = compute_user_objective(original_lp, upper_bound_); f_t lower = compute_user_objective(original_lp, lower_bound); - settings.log.printf( - "%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - symbol, - nodes_explored, - unexplored_nodes, - obj, - lower, - leaf_ptr->depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(stats_.start_time)); + settings.log.printf("%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + symbol, + nodes_explored, + unexplored_nodes, + obj, + lower, + leaf_ptr->depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(stats_.start_time)); send_solution = true; } @@ -523,8 +517,7 @@ template mip_status_t branch_and_bound_t::solve_root_relaxation( f_t& root_objective, lp_solution_t& root_relax_soln, - std::vector& root_vstatus, - std::vector& edge_norms) + std::vector& root_vstatus) { settings.log.printf("Solving LP root relaxation\n"); simplex_solver_settings_t lp_settings = settings; @@ -575,7 +568,6 @@ dual::status_t branch_and_bound_t::solve_leaf_lp( lp_problem_t& leaf_problem, std::vector& leaf_vstatus, lp_solution_t& leaf_solution, - std::vector& edge_norms, f_t upper_bound) { // Set the correct bounds for the leaf problem @@ -622,9 +614,6 @@ mip_status_t branch_and_bound_t::best_first_solve( i_t branch_var, f_t branch_var_val, std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; @@ -657,11 +646,12 @@ mip_status_t branch_and_bound_t::best_first_solve( while (gap > settings.absolute_mip_gap_tol && relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && heap.size() > 0) { - repair_heuristic_solutions(root_vstatus, edge_norms, lower_bound, incumbent, solution); + repair_heuristic_solutions(lower_bound, solution); // Get a node off the heap mip_node_t* node_ptr = heap.top(); - heap.pop(); // Remove node from the heap + heap.pop(); + f_t upper_bound = get_upper_bound(); if (upper_bound < node_ptr->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the current @@ -706,7 +696,7 @@ mip_status_t branch_and_bound_t::best_first_solve( std::vector& leaf_vstatus = node_ptr->vstatus; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); dual::status_t lp_status = - solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, upper_bound); + solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, upper_bound); nodes_explored++; @@ -742,7 +732,7 @@ mip_status_t branch_and_bound_t::best_first_solve( constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { add_feasible_solution( - node_ptr, leaf_objective, leaf_solution.x, incumbent, nodes_explored, heap.size(), 'B'); + node_ptr, leaf_objective, leaf_solution.x, nodes_explored, heap.size(), 'B'); } else if (leaf_objective <= upper_bound + fathom_tol) { mutex_pseudocosts.lock(); @@ -758,6 +748,8 @@ mip_status_t branch_and_bound_t::best_first_solve( node_ptr->get_down_child()); // the heap does not own the unique_ptr the tree does heap.push(node_ptr->get_up_child()); // the heap does not own the unique_ptr the tree does + node_ptr->set_status(node_status_t::COMPLETED); + } else { graphviz_node(settings, node_ptr, "fathomed", leaf_objective); std::vector*> stack; @@ -795,9 +787,6 @@ mip_status_t branch_and_bound_t::depth_first_solve( i_t branch_var, f_t branch_var_val, std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, mip_solution_t& solution, bool enable_reporting) { @@ -828,14 +817,14 @@ mip_status_t branch_and_bound_t::depth_first_solve( while (gap > settings.absolute_mip_gap_tol && relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && node_stack.size() > 0) { - repair_heuristic_solutions(root_vstatus, edge_norms, lower_bound, incumbent, solution); + repair_heuristic_solutions(lower_bound, solution); // Get a node off the stack mip_node_t* node_ptr = node_stack.back(); node_stack.pop_back(); f_t upper_bound = get_upper_bound(); - lower_bound = get_lower_bound(); + lower_bound = get_lower_bound(); gap = upper_bound - lower_bound; const i_t leaf_depth = node_ptr->depth; f_t now = toc(stats_.start_time); @@ -870,7 +859,7 @@ mip_status_t branch_and_bound_t::depth_first_solve( std::vector& leaf_vstatus = node_ptr->vstatus; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); dual::status_t lp_status = - solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, edge_norms, upper_bound); + solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, upper_bound); nodes_explored++; @@ -905,7 +894,8 @@ mip_status_t branch_and_bound_t::depth_first_solve( constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { - add_feasible_solution(node_ptr, leaf_objective, leaf_solution.x, incumbent, nodes_explored, node_stack.size(), 'D'); + add_feasible_solution( + node_ptr, leaf_objective, leaf_solution.x, nodes_explored, node_stack.size(), 'D'); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on @@ -920,8 +910,8 @@ mip_status_t branch_and_bound_t::depth_first_solve( branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); // Martin's child selection - const f_t down_val = std::floor(root_node.fractional_val); - const f_t up_val = std::ceil(root_node.fractional_val); + const f_t down_val = std::floor(root_node_->fractional_val); + const f_t up_val = std::ceil(root_node_->fractional_val); const f_t down_dist = leaf_solution.x[branch_var] - down_val; const f_t up_dist = up_val - leaf_solution.x[branch_var]; @@ -993,12 +983,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); - std::vector root_vstatus; - std::vector edge_norms; f_t root_objective; - mip_status_t root_status = - solve_root_relaxation(root_objective, root_relax_soln, root_vstatus, edge_norms); + mip_status_t root_status = solve_root_relaxation(root_objective, root_relax_soln, root_vstatus); if (root_status != mip_status_t::UNSET) { return root_status; } std::vector fractional; @@ -1028,7 +1015,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::OPTIMAL; } - pseudo_costs_t pc(original_lp.num_cols); + pc.initialize(original_lp.num_cols); strong_branching(original_lp, settings, stats_.start_time, @@ -1051,6 +1038,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut stats_.unexplored_nodes = 0; stats_.num_nodes = 1; + root_node_ = std::make_unique>(root_objective, root_vstatus); + graphviz_node(settings, root_node_.get(), "lower bound", root_objective); + root_node_->fractional_val = root_relax_soln.x[branch_var]; + root_node_->branch_var = branch_var; + if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { settings.log.printf("Using depth first search\n"); } else if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { @@ -1068,40 +1060,20 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut mutex_branching.unlock(); if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { - status = depth_first_solve(root_objective, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - edge_norms, - pc, - incumbent, - solution, - true); + status = depth_first_solve( + root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, true); } else { std::future diving_thread; if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { diving_thread = std::async(std::launch::async, [&]() { - return depth_first_solve(root_objective, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - edge_norms, - pc, - incumbent, - solution, - false); + return depth_first_solve( + root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, false); }); } - status = best_first_solve(root_objective, - branch_var, - root_relax_soln.x[branch_var], - root_vstatus, - edge_norms, - pc, - incumbent, - solution); + status = best_first_solve( + root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution); if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { // TODO: Instead of waiting, we should send a signal that we are already done in the main diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 55266e8140..44ad5a7e8c 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -25,12 +25,14 @@ #include #include -#include #include +#include +#include #include #include #include #include "cuopt/linear_programming/mip/solver_settings.hpp" +#include "dual_simplex/mip_node.hpp" namespace cuopt::linear_programming::dual_simplex { @@ -67,7 +69,7 @@ class branch_and_bound_t { std::vector& repaired_solution) const; f_t get_upper_bound(); - + f_t get_lower_bound(); // The main entry routine. Returns the solver status and populates solution with the incumbent. @@ -109,69 +111,63 @@ class branch_and_bound_t { // Note that floating point atomics are only supported in C++20 struct stats_t { - f_t start_time = 0.0; - f_t total_lp_solve_time = 0.0; - std::atomic nodes_explored = 0; - std::atomic unexplored_nodes = 0; - f_t total_lp_iters = 0; - std::atomic num_nodes = 0; + f_t start_time = 0.0; + f_t total_lp_solve_time = 0.0; + std::atomic nodes_explored = 0; + std::atomic unexplored_nodes = 0; + f_t total_lp_iters = 0; + std::atomic num_nodes = 0; } stats_; // Mutex for repair std::mutex mutex_repair; std::vector> repair_queue; - // Mutex for pseudocosts + // Pseudocosts std::mutex mutex_pseudocosts; + pseudo_costs_t pc; + + // Search tree + std::unique_ptr> root_node_; + std::vector root_vstatus; + std::vector edge_norms; - void repair_heuristic_solutions(const std::vector& root_vstatus, - const std::vector& edge_norms, - const f_t& lower_bound, - mip_solution_t& incumbent, + void repair_heuristic_solutions(const f_t& lower_bound, mip_solution_t& solution); mip_status_t best_first_solve(f_t root_objective, i_t branch_var, f_t branch_var_val, std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, mip_solution_t& solution); mip_status_t depth_first_solve(f_t root_objective, i_t branch_var, f_t branch_var_val, std::vector& root_vstatus, - std::vector& edge_norms, - pseudo_costs_t& pc, - mip_solution_t& incumbent, mip_solution_t& solution, bool enable_reporting); void branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, - std::vector& parent_vstatus); + const std::vector& parent_vstatus); void add_feasible_solution(mip_node_t* leaf_ptr, f_t leaf_objective, const std::vector& leaf_sol, - mip_solution_t& incumbent, i_t nodes_explored, i_t unexplored_nodes, char symbol); mip_status_t solve_root_relaxation(f_t& root_objective, lp_solution_t& root_relax_soln, - std::vector& root_vstatus, - std::vector& edge_norms); + std::vector& root_vstatus); dual::status_t solve_leaf_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, std::vector& leaf_vstatus, lp_solution_t& leaf_solution, - std::vector& edge_norms, f_t upper_bound); }; diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index fef67b9b14..6eb039be44 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -29,12 +29,25 @@ namespace cuopt::linear_programming::dual_simplex { template class pseudo_costs_t { public: + pseudo_costs_t() + : pseudo_cost_sum_down(0), + pseudo_cost_sum_up(0), + pseudo_cost_num_down(0), + pseudo_cost_num_up(0) + {} + explicit pseudo_costs_t(i_t num_variables) : pseudo_cost_sum_down(num_variables), pseudo_cost_sum_up(num_variables), pseudo_cost_num_down(num_variables), pseudo_cost_num_up(num_variables) - { + {} + + void initialize(i_t num_variables) { + pseudo_cost_sum_down.resize(num_variables); + pseudo_cost_sum_up.resize(num_variables); + pseudo_cost_num_down.resize(num_variables); + pseudo_cost_num_up.resize(num_variables); } void update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective); From 187e191a50d55214e3a147cef006db9439a5b1ef Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 26 Aug 2025 11:22:00 +0200 Subject: [PATCH 19/44] Moved search settings to simplex settings --- benchmarks/linear_programming/cuopt/run_mip.cpp | 14 +++++++------- .../linear_programming/mip/solver_settings.hpp | 14 +++++--------- cpp/src/dual_simplex/branch_and_bound.cpp | 14 ++++++-------- cpp/src/dual_simplex/branch_and_bound.hpp | 4 +--- cpp/src/dual_simplex/mip_node.hpp | 14 +++++++++++--- cpp/src/dual_simplex/simplex_solver_settings.hpp | 5 ++++- cpp/src/dual_simplex/solve.cpp | 8 ++------ cpp/src/dual_simplex/solve.hpp | 2 -- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 5 +---- cpp/src/mip/solver.cu | 4 ++-- cpp/tests/dual_simplex/unit_tests/solve.cpp | 9 ++------- 11 files changed, 41 insertions(+), 52 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 9868bfb8b0..cce97d308b 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -153,7 +153,7 @@ int run_single_file(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - cuopt::linear_programming::search_settings_t::strategy_t search_strategy) + cuopt::linear_programming::bnb_search_strategy_t search_strategy) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -170,7 +170,7 @@ int run_single_file(std::string file_path, } } - settings.search_settings.strategy = search_strategy; + settings.bnb_search_strategy = search_strategy; constexpr bool input_mps_strict = false; cuopt::mps_parser::mps_data_model_t mps_data_model; @@ -258,7 +258,7 @@ void run_single_file_mp(std::string file_path, bool write_log_file, bool log_to_console, double time_limit, - cuopt::linear_programming::search_settings_t::strategy_t search_strategy) + cuopt::linear_programming::bnb_search_strategy_t search_strategy) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -385,13 +385,13 @@ int main(int argc, char* argv[]) std::string search_strategy_cli = program.get("--search-strategy"); int gpu_id = program.get("--gpu"); - cuopt::linear_programming::search_settings_t::strategy_t search_strategy; + cuopt::linear_programming::bnb_search_strategy_t search_strategy; if (search_strategy_cli == "bfs") { - search_strategy = cuopt::linear_programming::search_settings_t::strategy_t::BEST_FIRST; + search_strategy = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST; } else if (search_strategy_cli == "bfs-diving") { - search_strategy = cuopt::linear_programming::search_settings_t::strategy_t::BEST_FIRST_DIVING; + search_strategy = cuopt::linear_programming::bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING; } else if (search_strategy_cli == "dfs") { - search_strategy = cuopt::linear_programming::search_settings_t::strategy_t::DEPTH_FIRST; + search_strategy = cuopt::linear_programming::bnb_search_strategy_t::DEPTH_FIRST; } else { std::cerr << "Invalid search strategy: " << search_strategy_cli << std::endl; exit(1); diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index c3637bdea5..a7d30baff2 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -35,14 +35,10 @@ struct benchmark_info_t { double objective_of_initial_population = std::numeric_limits::max(); }; -struct search_settings_t { - enum class strategy_t { - BEST_FIRST = 0, - DEPTH_FIRST = 1, - BEST_FIRST_DIVING = 2, - }; - - strategy_t strategy = strategy_t::BEST_FIRST; +enum class bnb_search_strategy_t { + BEST_FIRST = 0, + DEPTH_FIRST = 1, + MULTITHREADED_BEST_FIRST_WITH_DIVING = 2, }; // Forward declare solver_settings_t for friend class @@ -89,7 +85,7 @@ class mip_solver_settings_t { f_t relative_mip_gap = 1.0e-4; }; - search_settings_t search_settings; + bnb_search_strategy_t bnb_search_strategy = bnb_search_strategy_t::BEST_FIRST; /** * @brief Get the tolerance settings as a single structure. diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index aaa0015f98..6d5574c6c4 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -369,11 +369,9 @@ bool branch_and_bound_t::repair_solution( template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, - const simplex_solver_settings_t& solver_settings, - const search_settings_t strategy) + const simplex_solver_settings_t& solver_settings) : original_problem(user_problem), settings(solver_settings), - search_settings(strategy), original_lp(1, 1, 1), incumbent(1) { @@ -1043,9 +1041,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_node_->fractional_val = root_relax_soln.x[branch_var]; root_node_->branch_var = branch_var; - if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { + if (settings.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { settings.log.printf("Using depth first search\n"); - } else if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { + } else if (settings.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { settings.log.printf("Using best first search with diving\n"); } else { settings.log.printf("Using best first search\n"); @@ -1059,13 +1057,13 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut currently_branching = true; mutex_branching.unlock(); - if (search_settings.strategy == search_settings_t::strategy_t::DEPTH_FIRST) { + if (settings.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { status = depth_first_solve( root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, true); } else { std::future diving_thread; - if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { + if (settings.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { diving_thread = std::async(std::launch::async, [&]() { return depth_first_solve( root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, false); @@ -1075,7 +1073,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut status = best_first_solve( root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution); - if (search_settings.strategy == search_settings_t::strategy_t::BEST_FIRST_DIVING) { + if (settings.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { // TODO: Instead of waiting, we should send a signal that we are already done in the main // thread diving_thread.get(); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 44ad5a7e8c..175ca7a89a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -53,8 +53,7 @@ template class branch_and_bound_t { public: branch_and_bound_t(const user_problem_t& user_problem, - const simplex_solver_settings_t& solver_settings, - const search_settings_t strategy); + const simplex_solver_settings_t& solver_settings); // Set an initial guess based on the user_problem. This should be called before solve. void set_initial_guess(const std::vector& user_guess) { guess = user_guess; } @@ -78,7 +77,6 @@ class branch_and_bound_t { private: const user_problem_t& original_problem; const simplex_solver_settings_t settings; - search_settings_t search_settings; std::vector guess; diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index d32f48f850..5de4105652 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace cuopt::linear_programming::dual_simplex { @@ -33,6 +34,7 @@ enum class node_status_t : int { INTEGER_FEASIBLE = 2, // Node has an integer feasible solution INFEASIBLE = 3, // Node is infeasible FATHOMED = 4, // Node objective is greater than the upper bound + COMPLETED = 5, // Node has already been solved }; bool inactive_status(node_status_t status); @@ -80,6 +82,7 @@ class mip_node_t { children[1] = nullptr; } + void get_variable_bounds(std::vector& lower, std::vector& upper) const { // Apply the bounds at the current node @@ -145,10 +148,15 @@ class mip_node_t { } } + node_status_t set_status(node_status_t new_status) + { + return status.exchange(new_status, std::memory_order_seq_cst); + } + // outputs a stack containing inactive nodes in the tree that can be freed void set_status(node_status_t node_status, std::vector& stack) { - status = node_status; + status.store(node_status, std::memory_order_seq_cst); if (inactive_status(status)) { update_bound(); stack.push_back(this); @@ -156,7 +164,7 @@ class mip_node_t { mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr) { if (parent_ptr->is_inactive()) { - parent_ptr->status = node_status_t::FATHOMED; + parent_ptr->status.store(node_status_t::FATHOMED, std::memory_order_seq_cst); parent_ptr->update_bound(); stack.push_back(parent_ptr); } else { @@ -200,7 +208,7 @@ class mip_node_t { } } - node_status_t status; + std::atomic status; f_t lower_bound; i_t depth; i_t node_id; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index ead68022fc..ed76c32dad 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -17,9 +17,11 @@ #pragma once +#include #include #include + #include #include #include @@ -114,7 +116,8 @@ struct simplex_solver_settings_t { std::atomic* concurrent_halt; // if nullptr ignored, if !nullptr, 0 if solver should // continue, 1 if solver should halt - + bnb_search_strategy_t bnb_search_strategy = + bnb_search_strategy_t::BEST_FIRST; // Search strategy for B&B }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index a318b42a3d..83819578f8 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -266,12 +266,11 @@ lp_status_t solve_linear_program(const user_problem_t& user_problem, template i_t solve(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const search_settings_t search_settings, std::vector& primal_solution) { i_t status; if (is_mip(problem) && !settings.relaxation) { - branch_and_bound_t branch_and_bound(problem, settings, search_settings); + branch_and_bound_t branch_and_bound(problem, settings); mip_solution_t mip_solution(problem.num_cols); mip_status_t mip_status = branch_and_bound.solve(mip_solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -304,13 +303,12 @@ i_t solve(const user_problem_t& problem, template i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const search_settings_t search_settings, const std::vector& guess, mip_solution_t& solution) { i_t status; if (is_mip(problem)) { - branch_and_bound_t branch_and_bound(problem, settings, search_settings); + branch_and_bound_t branch_and_bound(problem, settings); branch_and_bound.set_initial_guess(guess); mip_status_t mip_status = branch_and_bound.solve(solution); if (mip_status == mip_status_t::OPTIMAL) { @@ -351,13 +349,11 @@ template lp_status_t solve_linear_program(const user_problem_t& use template int solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, - const search_settings_t search_settings, std::vector& primal_solution); template int solve_mip_with_guess( const user_problem_t& problem, const simplex_solver_settings_t& settings, - const search_settings_t search_settings, const std::vector& guess, mip_solution_t& solution); diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 6f51a0923f..493d7fcde4 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -68,14 +68,12 @@ i_t solve_mip(const user_problem_t& user_problem, mip_solution_t i_t solve_mip_with_guess(const user_problem_t& problem, const simplex_solver_settings_t& settings, - const search_settings_t search_settings, const std::vector& guess, mip_solution_t& solution); template i_t solve(const user_problem_t& user_problem, const simplex_solver_settings_t& settings, - const search_settings_t search_settings, std::vector& primal_solution); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 401433b385..8be8e88a9b 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -106,13 +106,10 @@ class sub_mip_recombiner_t : public recombiner_t { this->solution_callback(solution, objective); }; - search_settings_t search_settings; - // disable B&B logs, so that it is not interfering with the main B&B thread branch_and_bound_settings.log.log = false; dual_simplex::branch_and_bound_t branch_and_bound(branch_and_bound_problem, - branch_and_bound_settings, - search_settings); + branch_and_bound_settings); branch_and_bound_status = branch_and_bound.solve(branch_and_bound_solution); if (solution_vector.size() > 0) { cuopt_assert(fixed_assignment.size() == branch_and_bound_solution.x.size(), diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 94edeb5ae9..1bc81da391 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -168,6 +168,7 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; + branch_and_bound_settings.bnb_search_strategy = context.settings.bnb_search_strategy; if (context.settings.num_cpu_threads != -1) { branch_and_bound_settings.num_threads = std::max(1, context.settings.num_cpu_threads); @@ -189,8 +190,7 @@ solution_t mip_solver_t::run_solver() std::placeholders::_2); // Create the branch and bound object - branch_and_bound = std::make_unique>( - branch_and_bound_problem, branch_and_bound_settings, context.settings.search_settings); + branch_and_bound = std::make_unique>(branch_and_bound_problem, branch_and_bound_settings); // Set the primal heuristics -> branch and bound callback context.problem_ptr->branch_and_bound_callback = diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 9bdd673c62..4a5d04a673 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -88,8 +88,6 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::CONTINUOUS; user_problem.var_types[1] = dual_simplex::variable_type_t::CONTINUOUS; - search_settings_t search_settings; - dual_simplex::simplex_solver_settings_t settings; dual_simplex::lp_solution_t solution(user_problem.num_rows, user_problem.num_cols); EXPECT_EQ((dual_simplex::solve_linear_program(user_problem, settings, solution)), @@ -102,7 +100,7 @@ TEST(dual_simplex, chess_set) user_problem.var_types[0] = dual_simplex::variable_type_t::INTEGER; user_problem.var_types[1] = dual_simplex::variable_type_t::INTEGER; - EXPECT_EQ((dual_simplex::solve(user_problem, settings, search_settings, solution.x)), 0); + EXPECT_EQ((dual_simplex::solve(user_problem, settings, solution.x)), 0); } TEST(dual_simplex, burglar) @@ -163,12 +161,9 @@ TEST(dual_simplex, burglar) user_problem.var_types[j] = cuopt::linear_programming::dual_simplex::variable_type_t::INTEGER; } - search_settings_t search_settings; - cuopt::linear_programming::dual_simplex::simplex_solver_settings_t settings; std::vector solution(num_items); - EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve( - user_problem, settings, search_settings, solution)), + EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve(user_problem, settings, solution)), 0); double objective = 0.0; for (int j = 0; j < num_items; ++j) { From f4c150bd23fc4f20f4391b8fb82f2f15ad882b44 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 26 Aug 2025 16:01:59 +0200 Subject: [PATCH 20/44] Refactor based on reviewer's feedback Signed-off-by: nicolas --- .../linear_programming/cuopt/run_mip.cpp | 3 +- cpp/src/dual_simplex/branch_and_bound.cpp | 491 ++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 53 +- cpp/src/dual_simplex/mip_node.hpp | 15 +- cpp/src/dual_simplex/pseudo_costs.hpp | 14 +- .../dual_simplex/simplex_solver_settings.hpp | 1 - cpp/src/mip/solver.cu | 5 +- cpp/tests/dual_simplex/unit_tests/solve.cpp | 3 +- 8 files changed, 249 insertions(+), 336 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index cce97d308b..85f32a27ce 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -389,7 +389,8 @@ int main(int argc, char* argv[]) if (search_strategy_cli == "bfs") { search_strategy = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST; } else if (search_strategy_cli == "bfs-diving") { - search_strategy = cuopt::linear_programming::bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING; + search_strategy = + cuopt::linear_programming::bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING; } else if (search_strategy_cli == "dfs") { search_strategy = cuopt::linear_programming::bnb_search_strategy_t::DEPTH_FIRST; } else { diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 6d5574c6c4..c5ea2922f2 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -281,7 +281,7 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu f_t lower_bound = lower_bound_; mutex_lower.unlock(); mutex_branching.lock(); - bool currently_branching = currently_branching; + bool currently_branching = currently_branching_; mutex_branching.unlock(); if (currently_branching) { settings.log.printf( @@ -370,10 +370,7 @@ template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings) - : original_problem(user_problem), - settings(solver_settings), - original_lp(1, 1, 1), - incumbent(1) + : original_problem(user_problem), settings(solver_settings), original_lp(1, 1, 1), incumbent_(1) { stats_.start_time = tic(); convert_user_problem(original_problem, settings, original_lp, new_slacks); @@ -388,7 +385,7 @@ branch_and_bound_t::branch_and_bound_t( mutex_lower.unlock(); mutex_branching.lock(); - currently_branching = false; + currently_branching_ = false; mutex_branching.unlock(); } @@ -417,7 +414,7 @@ void branch_and_bound_t::repair_heuristic_solutions(const f_t& lower_b if (repaired_obj < upper_bound_) { upper_bound_ = repaired_obj; - incumbent.set_incumbent_solution(repaired_obj, repaired_solution); + incumbent_.set_incumbent_solution(repaired_obj, repaired_solution); f_t obj = compute_user_objective(original_lp, repaired_obj); f_t lower = compute_user_objective(original_lp, lower_bound); @@ -466,51 +463,6 @@ void branch_and_bound_t::branch(mip_node_t* parent_node, std::move(up_child)); // child pointers moved into the tree } -template -void branch_and_bound_t::add_feasible_solution(mip_node_t* leaf_ptr, - f_t leaf_objective, - const std::vector& leaf_sol, - i_t nodes_explored, - i_t unexplored_nodes, - char symbol) -{ - bool send_solution = false; - f_t lower_bound = get_lower_bound(); - - mutex_upper.lock(); - if (leaf_objective < upper_bound_) { - incumbent.set_incumbent_solution(leaf_objective, leaf_sol); - upper_bound_ = leaf_objective; - gap_ = upper_bound_ - lower_bound; - f_t obj = compute_user_objective(original_lp, upper_bound_); - f_t lower = compute_user_objective(original_lp, lower_bound); - settings.log.printf("%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - symbol, - nodes_explored, - unexplored_nodes, - obj, - lower, - leaf_ptr->depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(stats_.start_time)); - send_solution = true; - } - - if (send_solution && settings.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, incumbent.x, original_x); - settings.solution_callback(original_x, upper_bound_); - } - - mutex_upper.unlock(); - - graphviz_node(settings, leaf_ptr, "integer feasible", leaf_objective); - std::vector*> stack; - leaf_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); - remove_fathomed_nodes(stack); -} - template mip_status_t branch_and_bound_t::solve_root_relaxation( f_t& root_objective, @@ -561,13 +513,19 @@ mip_status_t branch_and_bound_t::solve_root_relaxation( } template -dual::status_t branch_and_bound_t::solve_leaf_lp( - mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - std::vector& leaf_vstatus, - lp_solution_t& leaf_solution, - f_t upper_bound) +mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + f_t upper_bound, + f_t lower_bound, + i_t nodes_explored, + i_t unexplored_nodes) { + logger_t log; + log.log = false; + f_t gap = upper_bound - lower_bound; + std::vector& leaf_vstatus = node_ptr->vstatus; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + // Set the correct bounds for the leaf problem leaf_problem.lower = original_lp.lower; leaf_problem.upper = original_lp.upper; @@ -603,16 +561,137 @@ dual::status_t branch_and_bound_t::solve_leaf_lp( stats_.total_lp_iters += node_iter; mutex_stats.unlock(); - return lp_status; + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { + node_ptr->lower_bound = inf; + std::vector*> stack; + node_ptr->set_status(node_status_t::INFEASIBLE, stack); + graphviz_node(settings, node_ptr, "infeasible", 0.0); + remove_fathomed_nodes(stack); + // Node was infeasible. Do not branch + } else if (lp_status == dual::status_t::CUTOFF) { + node_ptr->lower_bound = upper_bound; + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "cut off", leaf_objective); + remove_fathomed_nodes(stack); + // Node was cut off. Do not branch + } else if (lp_status == dual::status_t::OPTIMAL) { + // LP was feasible + std::vector fractional; + const i_t leaf_fractional = + fractional_variables(settings, leaf_solution.x, var_types, fractional); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + + mutex_pseudocosts.lock(); + pc.update_pseudo_costs(node_ptr, leaf_objective); + mutex_pseudocosts.unlock(); + + node_ptr->lower_bound = leaf_objective; + + constexpr f_t fathom_tol = 1e-5; + if (leaf_fractional == 0) { + bool send_solution = false; + + mutex_upper.lock(); + if (leaf_objective < upper_bound_) { + incumbent_.set_incumbent_solution(leaf_objective, leaf_solution.x); + upper_bound_ = leaf_objective; + gap = upper_bound_ - lower_bound; + f_t obj = compute_user_objective(original_lp, upper_bound_); + f_t lower = compute_user_objective(original_lp, lower_bound); + settings.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + unexplored_nodes, + obj, + lower, + node_ptr->depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(stats_.start_time)); + send_solution = true; + } + + mutex_upper.unlock(); + + if (send_solution && settings.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem, original_lp, incumbent_.x, original_x); + settings.solution_callback(original_x, upper_bound_); + } + + if (send_solution) { + mutex_gap.lock(); + gap_ = gap; + mutex_gap.unlock(); + } + + graphviz_node(settings, node_ptr, "integer feasible", leaf_objective); + std::vector*> stack; + node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); + remove_fathomed_nodes(stack); + + } else if (leaf_objective <= upper_bound + fathom_tol) { + // Choose fractional variable to branch on + mutex_pseudocosts.lock(); + const i_t branch_var = pc.variable_selection( + fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + mutex_pseudocosts.unlock(); + + assert(leaf_vstatus.size() == leaf_problem.num_cols); + branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); + node_ptr->set_status(node_status_t::HAS_CHILDREN); + + } else { + graphviz_node(settings, node_ptr, "fathomed", leaf_objective); + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + remove_fathomed_nodes(stack); + } + } else { + graphviz_node(settings, node_ptr, "numerical", 0.0); + settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", lp_status); + return mip_status_t::NUMERICAL; + } + + return mip_status_t::UNSET; +} + +template +void branch_and_bound_t::report(f_t last_log, + i_t nodes_explored, + i_t nodes_unexplored, + f_t gap, + f_t upper_bound, + f_t lower_bound, + i_t leaf_depth) +{ + f_t now = toc(stats_.start_time); + f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || + nodes_explored < 1000) && + (time_since_log >= 1) || + (time_since_log > 60) || now > settings.time_limit) { + settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + nodes_unexplored, + compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound), + leaf_depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp, upper_bound), + compute_user_objective(original_lp, lower_bound)) + .c_str(), + now); + last_log = tic(); + } } template -mip_status_t branch_and_bound_t::best_first_solve( - f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - mip_solution_t& solution) +mip_status_t branch_and_bound_t::explore_tree(mip_node_t* start_node, + f_t root_objective, + mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; logger_t log; @@ -626,12 +705,8 @@ mip_status_t branch_and_bound_t::best_first_solve( std::priority_queue*, std::vector*>, decltype(compare)> heap(compare); - mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", lower_bound_); - - branch(&root_node, branch_var, branch_var_val, root_vstatus); - heap.push(root_node.get_down_child()); // the heap does not own the unique_ptr the tree does - heap.push(root_node.get_up_child()); // the heap does not own the unqiue_ptr the tree does + heap.push(start_node->get_down_child()); // the heap does not own the unique_ptr the tree does + heap.push(start_node->get_up_child()); // the heap does not own the unqiue_ptr the tree does // Make a copy of the original LP. We will modify its bounds at each leaf lp_problem_t leaf_problem = original_lp; @@ -649,6 +724,7 @@ mip_status_t branch_and_bound_t::best_first_solve( // Get a node off the heap mip_node_t* node_ptr = heap.top(); heap.pop(); + nodes_explored++; f_t upper_bound = get_upper_bound(); if (upper_bound < node_ptr->lower_bound) { @@ -661,105 +737,30 @@ mip_status_t branch_and_bound_t::best_first_solve( continue; } mutex_lower.lock(); - lower_bound_ = node_ptr->lower_bound; + lower_bound = lower_bound_ = node_ptr->lower_bound; mutex_lower.unlock(); - gap = upper_bound - lower_bound; - const i_t leaf_depth = node_ptr->depth; - f_t now = toc(stats_.start_time); - f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || - nodes_explored < 1000) && - (time_since_log >= 1) || - (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - heap.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), - leaf_depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - now); - last_log = tic(); - } - if (now > settings.time_limit) { + mutex_gap.lock(); + gap_ = gap = upper_bound - lower_bound; + mutex_gap.unlock(); + + report(last_log, nodes_explored, heap.size(), gap, upper_bound, lower_bound, node_ptr->depth); + + if (toc(stats_.start_time) > settings.time_limit) { settings.log.printf("Hit time limit. Stoppping\n"); status = mip_status_t::TIME_LIMIT; break; } - std::vector& leaf_vstatus = node_ptr->vstatus; - lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); - dual::status_t lp_status = - solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, upper_bound); + status = + solve_leaf_lp(node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, heap.size()); - nodes_explored++; + if (status == mip_status_t::NUMERICAL) { break; } - if (lp_status == dual::status_t::DUAL_UNBOUNDED) { - node_ptr->lower_bound = inf; - std::vector*> stack; - node_ptr->set_status(node_status_t::INFEASIBLE, stack); - graphviz_node(settings, node_ptr, "infeasible", 0.0); - remove_fathomed_nodes(stack); - // Node was infeasible. Do not branch - } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = upper_bound; - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "cut off", leaf_objective); - remove_fathomed_nodes(stack); - // Node was cut off. Do not branch - } else if (lp_status == dual::status_t::OPTIMAL) { - // LP was feasible - std::vector fractional; - const i_t leaf_fractional = - fractional_variables(settings, leaf_solution.x, var_types, fractional); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "lower bound", leaf_objective); - - mutex_pseudocosts.lock(); - pc.update_pseudo_costs(node_ptr, leaf_objective); - mutex_pseudocosts.unlock(); - - node_ptr->lower_bound = leaf_objective; - - constexpr f_t fathom_tol = 1e-5; - if (leaf_fractional == 0) { - add_feasible_solution( - node_ptr, leaf_objective, leaf_solution.x, nodes_explored, heap.size(), 'B'); - - } else if (leaf_objective <= upper_bound + fathom_tol) { - mutex_pseudocosts.lock(); - // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - mutex_pseudocosts.unlock(); - - assert(leaf_vstatus.size() == leaf_problem.num_cols); - - branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); - heap.push( - node_ptr->get_down_child()); // the heap does not own the unique_ptr the tree does - heap.push(node_ptr->get_up_child()); // the heap does not own the unique_ptr the tree does - - node_ptr->set_status(node_status_t::COMPLETED); - - } else { - graphviz_node(settings, node_ptr, "fathomed", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - remove_fathomed_nodes(stack); - } - } else { - graphviz_node(settings, node_ptr, "numerical", 0.0); - settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", - lp_status); - status = mip_status_t::NUMERICAL; - break; + if (node_ptr->status == node_status_t::HAS_CHILDREN) { + // the heap does not own the unique_ptr the tree does + heap.push(node_ptr->get_down_child()); + heap.push(node_ptr->get_up_child()); } } @@ -768,7 +769,7 @@ mip_status_t branch_and_bound_t::best_first_solve( if (stats_.unexplored_nodes == 0) { mutex_lower.lock(); - lower_bound = lower_bound_ = root_node.lower_bound; + lower_bound = lower_bound_ = root_node_->lower_bound; mutex_lower.unlock(); mutex_gap.lock(); @@ -780,13 +781,12 @@ mip_status_t branch_and_bound_t::best_first_solve( } template -mip_status_t branch_and_bound_t::depth_first_solve( - f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - mip_solution_t& solution, - bool enable_reporting) +mip_status_t branch_and_bound_t::dive(f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + mip_solution_t& solution, + bool enable_reporting) { mip_status_t status = mip_status_t::UNSET; @@ -825,118 +825,36 @@ mip_status_t branch_and_bound_t::depth_first_solve( lower_bound = get_lower_bound(); gap = upper_bound - lower_bound; const i_t leaf_depth = node_ptr->depth; - f_t now = toc(stats_.start_time); if (enable_reporting) { - f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || - nodes_explored < 1000) && - (time_since_log >= 1) || - (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - node_stack.size(), - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), - leaf_depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - now); - last_log = tic(); - } + report( + last_log, nodes_explored, node_stack.size(), gap, upper_bound, lower_bound, leaf_depth); } - if (now > settings.time_limit) { + if (toc(stats_.start_time) > settings.time_limit) { if (enable_reporting) { settings.log.printf("Hit time limit. Stoppping\n"); } status = mip_status_t::TIME_LIMIT; break; } - std::vector& leaf_vstatus = node_ptr->vstatus; - lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); - dual::status_t lp_status = - solve_leaf_lp(node_ptr, leaf_problem, leaf_vstatus, leaf_solution, upper_bound); - - nodes_explored++; - - if (lp_status == dual::status_t::DUAL_UNBOUNDED) { - node_ptr->lower_bound = inf; - std::vector*> stack; - node_ptr->set_status(node_status_t::INFEASIBLE, stack); - graphviz_node(settings, node_ptr, "infeasible", 0.0); - remove_fathomed_nodes(stack); - // Node was infeasible. Do not branch - } else if (lp_status == dual::status_t::CUTOFF) { - node_ptr->lower_bound = upper_bound; - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "cut off", leaf_objective); - remove_fathomed_nodes(stack); - // Node was cut off. Do not branch - } else if (lp_status == dual::status_t::OPTIMAL) { - // LP was feasible - std::vector fractional; - const i_t leaf_fractional = - fractional_variables(settings, leaf_solution.x, var_types, fractional); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "lower bound", leaf_objective); - - mutex_pseudocosts.lock(); - pc.update_pseudo_costs(node_ptr, leaf_objective); - mutex_pseudocosts.unlock(); + status = solve_leaf_lp( + node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, node_stack.size()); + if (status == mip_status_t::NUMERICAL) { break; } - node_ptr->lower_bound = leaf_objective; - - constexpr f_t fathom_tol = 1e-5; - if (leaf_fractional == 0) { - add_feasible_solution( - node_ptr, leaf_objective, leaf_solution.x, nodes_explored, node_stack.size(), 'D'); - - } else if (leaf_objective <= upper_bound + fathom_tol) { - // Choose fractional variable to branch on - - mutex_pseudocosts.lock(); - const i_t branch_var = pc.variable_selection( - fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - mutex_pseudocosts.unlock(); - - assert(leaf_vstatus.size() == leaf_problem.num_cols); - - branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); - - // Martin's child selection - const f_t down_val = std::floor(root_node_->fractional_val); - const f_t up_val = std::ceil(root_node_->fractional_val); - const f_t down_dist = leaf_solution.x[branch_var] - down_val; - const f_t up_dist = up_val - leaf_solution.x[branch_var]; - - if (down_dist < up_dist) { - node_stack.push_back(node_ptr->get_up_child()); - node_stack.push_back(node_ptr->get_down_child()); - } else { - node_stack.push_back(node_ptr->get_down_child()); - node_stack.push_back(node_ptr->get_up_child()); - } + if (node_ptr->status == node_status_t::HAS_CHILDREN) { + // Martin's child selection + const f_t down_val = std::floor(root_node_->fractional_val); + const f_t up_val = std::ceil(root_node_->fractional_val); + const f_t down_dist = node_ptr->get_down_child()->fractional_val - down_val; + const f_t up_dist = up_val - node_ptr->get_down_child()->fractional_val; + if (down_dist < up_dist) { + node_stack.push_back(node_ptr->get_up_child()); + node_stack.push_back(node_ptr->get_down_child()); } else { - graphviz_node(settings, node_ptr, "fathomed", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - remove_fathomed_nodes(stack); - } - } else { - graphviz_node(settings, node_ptr, "numerical", 0.0); - - if (enable_reporting) { - settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", - lp_status); + node_stack.push_back(node_ptr->get_down_child()); + node_stack.push_back(node_ptr->get_up_child()); } - - status = mip_status_t::NUMERICAL; - break; } } @@ -962,7 +880,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { mip_status_t status = mip_status_t::UNSET; - mip_solution_t incumbent(original_lp.num_cols); if (guess.size() != 0) { std::vector crushed_guess; crush_primal_solution(original_problem, original_lp, guess, new_slacks, crushed_guess); @@ -974,7 +891,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (feasible) { const f_t computed_obj = compute_objective(original_lp, crushed_guess); mutex_upper.lock(); - incumbent.set_incumbent_solution(computed_obj, crushed_guess); + incumbent_.set_incumbent_solution(computed_obj, crushed_guess); upper_bound_ = computed_obj; mutex_upper.unlock(); } @@ -992,12 +909,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fractional == 0) { mutex_upper.lock(); - incumbent.set_incumbent_solution(root_objective, root_relax_soln.x); + incumbent_.set_incumbent_solution(root_objective, root_relax_soln.x); upper_bound_ = root_objective; mutex_upper.unlock(); // We should be done here - uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); - solution.objective = incumbent.objective; + uncrush_primal_solution(original_problem, original_lp, incumbent_.x, solution.x); + solution.objective = incumbent_.objective; solution.lower_bound = lower_bound_; solution.nodes_explored = 0; solution.simplex_iterations = root_relax_soln.iterations; @@ -1041,9 +958,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_node_->fractional_val = root_relax_soln.x[branch_var]; root_node_->branch_var = branch_var; + branch(root_node_.get(), branch_var, root_relax_soln.x[branch_var], root_vstatus); + if (settings.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { settings.log.printf("Using depth first search\n"); - } else if (settings.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { + } else if (settings.bnb_search_strategy == + bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { settings.log.printf("Using best first search with diving\n"); } else { settings.log.printf("Using best first search\n"); @@ -1054,26 +974,27 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut " Time \n"); mutex_branching.lock(); - currently_branching = true; + currently_branching_ = true; mutex_branching.unlock(); if (settings.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { - status = depth_first_solve( - root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, true); + status = + dive(root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, true); } else { std::future diving_thread; - if (settings.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { + if (settings.bnb_search_strategy == + bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { diving_thread = std::async(std::launch::async, [&]() { - return depth_first_solve( + return dive( root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, false); }); } - status = best_first_solve( - root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution); + status = explore_tree(root_node_.get(), root_objective, solution); - if (settings.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { + if (settings.bnb_search_strategy == + bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { // TODO: Instead of waiting, we should send a signal that we are already done in the main // thread diving_thread.get(); @@ -1081,7 +1002,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } mutex_branching.lock(); - currently_branching = false; + currently_branching_ = false; mutex_branching.unlock(); settings.log.printf( @@ -1118,8 +1039,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - uncrush_primal_solution(original_problem, original_lp, incumbent.x, solution.x); - solution.objective = incumbent.objective; + uncrush_primal_solution(original_problem, original_lp, incumbent_.x, solution.x); + solution.objective = incumbent_.objective; solution.lower_bound = get_lower_bound(); solution.nodes_explored = stats_.nodes_explored; solution.simplex_iterations = stats_.total_lp_iters; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 175ca7a89a..7e3875eaa8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -93,7 +93,7 @@ class branch_and_bound_t { // Global variable for upper bound f_t upper_bound_; // Global variable for incumbent. The incumbent should be updated with the upper bound - mip_solution_t incumbent; + mip_solution_t incumbent_; // Mutex for gap std::mutex mutex_gap; @@ -102,7 +102,7 @@ class branch_and_bound_t { // Mutex for branching std::mutex mutex_branching; - bool currently_branching; + bool currently_branching_; // Global variable for stats std::mutex mutex_stats; @@ -130,43 +130,42 @@ class branch_and_bound_t { std::vector root_vstatus; std::vector edge_norms; - void repair_heuristic_solutions(const f_t& lower_bound, - mip_solution_t& solution); + void repair_heuristic_solutions(const f_t& lower_bound, mip_solution_t& solution); - mip_status_t best_first_solve(f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - mip_solution_t& solution); + void report(f_t last_log, + i_t nodes_explored, + i_t nodes_unexplored, + f_t gap, + f_t upper_bound, + f_t lower_bound, + i_t leaf_depth); - mip_status_t depth_first_solve(f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, - mip_solution_t& solution, - bool enable_reporting); + mip_status_t explore_tree(mip_node_t* start_node, + f_t root_objective, + mip_solution_t& solution); + + mip_status_t dive(f_t root_objective, + i_t branch_var, + f_t branch_var_val, + std::vector& root_vstatus, + mip_solution_t& solution, + bool enable_reporting); void branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, const std::vector& parent_vstatus); - void add_feasible_solution(mip_node_t* leaf_ptr, - f_t leaf_objective, - const std::vector& leaf_sol, - i_t nodes_explored, - i_t unexplored_nodes, - char symbol); - mip_status_t solve_root_relaxation(f_t& root_objective, lp_solution_t& root_relax_soln, std::vector& root_vstatus); - dual::status_t solve_leaf_lp(mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - std::vector& leaf_vstatus, - lp_solution_t& leaf_solution, - f_t upper_bound); + mip_status_t solve_leaf_lp(mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + f_t upper_bound, + f_t lower_bound, + i_t nodes_explored, + i_t unexplored_nodes); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 5de4105652..5413798ae4 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -20,11 +20,11 @@ #include #include +#include #include #include #include #include -#include namespace cuopt::linear_programming::dual_simplex { @@ -34,7 +34,7 @@ enum class node_status_t : int { INTEGER_FEASIBLE = 2, // Node has an integer feasible solution INFEASIBLE = 3, // Node is infeasible FATHOMED = 4, // Node objective is greater than the upper bound - COMPLETED = 5, // Node has already been solved + HAS_CHILDREN = 5, // Node has children to explore }; bool inactive_status(node_status_t status); @@ -82,7 +82,6 @@ class mip_node_t { children[1] = nullptr; } - void get_variable_bounds(std::vector& lower, std::vector& upper) const { // Apply the bounds at the current node @@ -102,15 +101,9 @@ class mip_node_t { } } - mip_node_t* get_down_child() const - { - return children[0].get(); - } + mip_node_t* get_down_child() const { return children[0].get(); } - mip_node_t* get_up_child() const - { - return children[1].get(); - } + mip_node_t* get_up_child() const { return children[1].get(); } void add_children(std::unique_ptr&& down_child, std::unique_ptr&& up_child) diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 6eb039be44..5065b8525d 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -30,20 +30,20 @@ template class pseudo_costs_t { public: pseudo_costs_t() - : pseudo_cost_sum_down(0), - pseudo_cost_sum_up(0), - pseudo_cost_num_down(0), - pseudo_cost_num_up(0) - {} + : pseudo_cost_sum_down(0), pseudo_cost_sum_up(0), pseudo_cost_num_down(0), pseudo_cost_num_up(0) + { + } explicit pseudo_costs_t(i_t num_variables) : pseudo_cost_sum_down(num_variables), pseudo_cost_sum_up(num_variables), pseudo_cost_num_down(num_variables), pseudo_cost_num_up(num_variables) - {} + { + } - void initialize(i_t num_variables) { + void initialize(i_t num_variables) + { pseudo_cost_sum_down.resize(num_variables); pseudo_cost_sum_up.resize(num_variables); pseudo_cost_num_down.resize(num_variables); diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index ed76c32dad..5f8fcce7fe 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -21,7 +21,6 @@ #include #include - #include #include #include diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 1bc81da391..e7b0d3f94a 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -69,7 +69,7 @@ template struct branch_and_bound_solution_helper_t { branch_and_bound_solution_helper_t(diversity_manager_t* dm, dual_simplex::simplex_solver_settings_t& settings) - : dm(dm), settings_(settings){}; + : dm(dm), settings_(settings) {}; void solution_callback(std::vector& solution, f_t objective) { @@ -190,7 +190,8 @@ solution_t mip_solver_t::run_solver() std::placeholders::_2); // Create the branch and bound object - branch_and_bound = std::make_unique>(branch_and_bound_problem, branch_and_bound_settings); + branch_and_bound = std::make_unique>( + branch_and_bound_problem, branch_and_bound_settings); // Set the primal heuristics -> branch and bound callback context.problem_ptr->branch_and_bound_callback = diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 4a5d04a673..61bb04dee4 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -163,8 +163,7 @@ TEST(dual_simplex, burglar) cuopt::linear_programming::dual_simplex::simplex_solver_settings_t settings; std::vector solution(num_items); - EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve(user_problem, settings, solution)), - 0); + EXPECT_EQ((cuopt::linear_programming::dual_simplex::solve(user_problem, settings, solution)), 0); double objective = 0.0; for (int j = 0; j < num_items; ++j) { objective += value[j] * solution[j]; From a8aacc530af67803a2e37d650abef9da4d39ab68 Mon Sep 17 00:00:00 2001 From: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:31:11 -0500 Subject: [PATCH 21/44] Support cuda 12.9 (#269) Support cuda 12.9 ## Issue closes #211 Authors: - Ramakrishnap (https://github.com/rgsl888prabhu) Approvers: - Trevor McKay (https://github.com/tmckayus) URL: https://github.com/NVIDIA/cuopt/pull/269 --- .../workflows/build_test_publish_images.yaml | 2 +- .../workflows/self_hosted_service_test.yaml | 2 +- CONTRIBUTING.md | 2 +- README.md | 6 +- ci/build-service.Dockerfile | 75 ------------------- ci/release/update-version.sh | 2 +- ...64.yaml => all_cuda-129_arch-aarch64.yaml} | 4 +- ..._64.yaml => all_cuda-129_arch-x86_64.yaml} | 4 +- cpp/CMakeLists.txt | 2 +- dependencies.yaml | 2 +- docs/cuopt/source/cuopt-c/quick-start.rst | 2 +- .../cuopt/source/cuopt-python/quick-start.rst | 10 +-- .../cuopt/source/cuopt-server/quick-start.rst | 10 +-- docs/cuopt/source/system-requirements.rst | 4 +- helmchart/cuopt-server/values.yaml | 2 +- 15 files changed, 27 insertions(+), 102 deletions(-) delete mode 100644 ci/build-service.Dockerfile rename conda/environments/{all_cuda-128_arch-aarch64.yaml => all_cuda-129_arch-aarch64.yaml} (96%) rename conda/environments/{all_cuda-128_arch-x86_64.yaml => all_cuda-129_arch-x86_64.yaml} (96%) diff --git a/.github/workflows/build_test_publish_images.yaml b/.github/workflows/build_test_publish_images.yaml index fcf85651de..af066e41ee 100644 --- a/.github/workflows/build_test_publish_images.yaml +++ b/.github/workflows/build_test_publish_images.yaml @@ -32,7 +32,7 @@ on: description: 'JSON array of architectures to build for' cuda_ver: type: string - default: '["12.8.0"]' + default: '["12.9.0"]' description: 'JSON array of CUDA versions to build for' python_ver: type: string diff --git a/.github/workflows/self_hosted_service_test.yaml b/.github/workflows/self_hosted_service_test.yaml index 8d3f6215b1..3b12dcecb2 100644 --- a/.github/workflows/self_hosted_service_test.yaml +++ b/.github/workflows/self_hosted_service_test.yaml @@ -59,7 +59,7 @@ jobs: runs-on: linux-amd64-gpu-l4-latest-1 strategy: matrix: - ctk: ["12.8.0"] + ctk: ["12.9.0"] linux_ver: ["ubuntu24.04"] py_ver: ["3.12"] container: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3dd28dad30..133c86e28b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,7 +70,7 @@ library features. The following instructions are for building with a conda envir CUDA/GPU Runtime: -* CUDA 12.8 +* CUDA 12.9 * Volta architecture or better ([Compute Capability](https://docs.nvidia.com/deploy/cuda-compatibility/) >=7.0) Python: diff --git a/README.md b/README.md index fcab5ddc21..f7390b9586 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ on the major version of CUDA available in your environment: For CUDA 12.x: ```bash -pip install --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12==25.10.* cuopt-sh-client==25.10.* nvidia-cuda-runtime-cu12==12.8.* +pip install --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12==25.10.* cuopt-sh-client==25.10.* nvidia-cuda-runtime-cu12==12.9.* ``` Development wheels are available as nightlies, please update `--extra-index-url` to `https://pypi.anaconda.org/rapidsai-wheels-nightly/simple/` to install latest nightly packages. @@ -85,10 +85,10 @@ of our latest development branch. Just replace `-c rapidsai` with `-c rapidsai-n Users can pull the cuOpt container from the NVIDIA container registry. ```bash -docker pull nvidia/cuopt:latest-cuda12.8-py312 +docker pull nvidia/cuopt:latest-cuda12.9-py312 ``` -Note: The ``latest`` tag is the latest stable release of cuOpt. If you want to use a specific version, you can use the ``-cuda12.8-py312`` tag. For example, to use cuOpt 25.5.0, you can use the ``25.5.0-cuda12.8-py312`` tag. Please refer to `cuOpt dockerhub page `_ for the list of available tags. +Note: The ``latest`` tag is the latest stable release of cuOpt. If you want to use a specific version, you can use the ``-cuda12.9-py312`` tag. For example, to use cuOpt 25.5.0, you can use the ``25.5.0-cuda12.8-py312`` tag. Please refer to `cuOpt dockerhub page `_ for the list of available tags. More information about the cuOpt container can be found [here](https://docs.nvidia.com/cuopt/user-guide/latest/cuopt-server/quick-start.html#container-from-docker-hub). diff --git a/ci/build-service.Dockerfile b/ci/build-service.Dockerfile deleted file mode 100644 index d1e282cee4..0000000000 --- a/ci/build-service.Dockerfile +++ /dev/null @@ -1,75 +0,0 @@ -# syntax=docker/dockerfile:1.2 -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -ARG arch=amd -ARG cuda_ver=12.5.1 - -# To copy cuda files -FROM nvcr.io/nvidia/cuda:${cuda_ver}-runtime-ubuntu22.04 AS cuda-env - - -# To copy nvvm -FROM rapidsai/ci-wheel:cuda${cuda_ver}-rockylinux8-py3.11 AS cuopt_build - - -# Install cuOpt -FROM python:3.12.8-slim-bookworm AS install-env - -COPY --chown=nvs:nvs ./wheels /tmp/wheels/ -ARG cuda-suffix=cu12 - -RUN apt-get update && apt-get install -y git gcc - -ENV PIP_EXTRA_INDEX_URL="https://pypi.nvidia.com https://pypi.anaconda.org/rapidsai-wheels-nightly/simple" - -RUN python -m pip install nvidia-cuda-runtime-cu12==12.5.* -RUN python -m pip install /tmp/wheels/cuopt_mps_parser* /tmp/wheels/cuopt_${cuda_suffix}* /tmp/wheels/cuopt_server_${cuda_suffix}* -RUN python -m pip uninstall setuptools -y - - -# Build release container -FROM nvcr.io/nvidian/distroless/python:3.12-v3.4.4-${arch}64 - -ARG cuda_ver=12.5.1 -COPY --from=install-env --chown=nvs:nvs \ - /usr/local/lib/python3.12/site-packages \ - /usr/local/lib/python3.12/dist-packages - -COPY --from=cuda-env --chown=nvs:nvs \ - /usr/local/cuda-* \ - /usr/local/cuda - -COPY --from=cuopt_build --chown=nvs:nvs /usr/local/cuda/nvvm/ /usr/local/cuda/nvvm/ - -ARG nspect_id -ARG server_port=5000 - -ENV CUOPT_SERVER_PORT=${server_port} - -ENV CUOPT_SERVER_NSPECT_ID=${nspect_id} - -ENV RMM_DEBUG_LOG_FILE=/tmp/rmm_log.txt -ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64 -ENV CUPY_CACHE_DIR=/tmp/.cupy - -WORKDIR /cache - -COPY ./LICENSE /cache/LICENSE -COPY ./container-builder/README.md /cache/ -COPY ./container-builder/CHANGELOG.md /cache/ -COPY ./git_info.txt /cache/ - -CMD ["python3", "-m", "cuopt_server.cuopt_service"] diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 3d5b1f2413..9c5f21a8ee 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -112,7 +112,7 @@ sed_runner "/^set(cuopt_version/ s/[0-9][0-9].[0-9][0-9].[0-9][0-9]/${NEXT_FULL_ sed_runner 's/'"cuopt_version: \"[0-9][0-9].[0-9][0-9]\""'/'"cuopt_version: \"${NEXT_SHORT_TAG}\""'/g' .github/workflows/nightly.yaml # Update Helm chart files -sed_runner 's/\(tag: "\)[0-9][0-9]\.[0-9]\+\.[0-9]\+\(-cuda12\.8-py3\.12"\)/\1'${DOCKER_TAG}'\2/g' helmchart/cuopt-server/values.yaml +sed_runner 's/\(tag: "\)[0-9][0-9]\.[0-9]\+\.[0-9]\+\(-cuda12\.9-py3\.12"\)/\1'${DOCKER_TAG}'\2/g' helmchart/cuopt-server/values.yaml sed_runner 's/\(appVersion: \)[0-9][0-9]\.[0-9]\+\.[0-9]\+/\1'${DOCKER_TAG}'/g' helmchart/cuopt-server/Chart.yaml sed_runner 's/\(version: \)[0-9][0-9]\.[0-9]\+\.[0-9]\+/\1'${DOCKER_TAG}'/g' helmchart/cuopt-server/Chart.yaml diff --git a/conda/environments/all_cuda-128_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml similarity index 96% rename from conda/environments/all_cuda-128_arch-aarch64.yaml rename to conda/environments/all_cuda-129_arch-aarch64.yaml index ba6b6945e3..3565eae6a1 100644 --- a/conda/environments/all_cuda-128_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -18,7 +18,7 @@ dependencies: - cuda-nvtx-dev - cuda-nvvm - cuda-sanitizer-api -- cuda-version=12.8 +- cuda-version=12.9 - cudf==25.10.*,>=0.0.0a0 - cupy>=12.0.0 - cuvs==25.10.*,>=0.0.0a0 @@ -85,4 +85,4 @@ dependencies: - nvidia_sphinx_theme - swagger-plugin-for-sphinx - veroviz -name: all_cuda-128_arch-aarch64 +name: all_cuda-129_arch-aarch64 diff --git a/conda/environments/all_cuda-128_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml similarity index 96% rename from conda/environments/all_cuda-128_arch-x86_64.yaml rename to conda/environments/all_cuda-129_arch-x86_64.yaml index f91c229570..04132f8c7a 100644 --- a/conda/environments/all_cuda-128_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -18,7 +18,7 @@ dependencies: - cuda-nvtx-dev - cuda-nvvm - cuda-sanitizer-api -- cuda-version=12.8 +- cuda-version=12.9 - cudf==25.10.*,>=0.0.0a0 - cupy>=12.0.0 - cuvs==25.10.*,>=0.0.0a0 @@ -85,4 +85,4 @@ dependencies: - nvidia_sphinx_theme - swagger-plugin-for-sphinx - veroviz -name: all_cuda-128_arch-x86_64 +name: all_cuda-129_arch-x86_64 diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 53071afaed..290016f902 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -104,7 +104,7 @@ message("-- Building for GPU_ARCHS = ${CMAKE_CUDA_ARCHITECTURES}") # make the flags global in order to propagate flags to test cmake files set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr --expt-extended-lambda") -if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.8 AND CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 13.0) +if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.9 AND CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 13.0) set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -static-global-template-stub=false") endif() list(APPEND CUOPT_CUDA_FLAGS -Werror=cross-execution-space-call -Wno-deprecated-declarations -Xcompiler=-Werror) diff --git a/dependencies.yaml b/dependencies.yaml index 07de525333..ae7c345a79 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -19,7 +19,7 @@ files: all: output: conda matrix: - cuda: ["12.8"] + cuda: ["12.9"] arch: [x86_64, aarch64] includes: - build_common diff --git a/docs/cuopt/source/cuopt-c/quick-start.rst b/docs/cuopt/source/cuopt-c/quick-start.rst index 7ed1040188..624ca6a2ab 100644 --- a/docs/cuopt/source/cuopt-c/quick-start.rst +++ b/docs/cuopt/source/cuopt-c/quick-start.rst @@ -20,7 +20,7 @@ This wheel is a Python wrapper around the C++ library and eases installation and # This is a deprecated module and no longer used, but it shares the same name for the CLI, so we need to uninstall it first if it exists. pip uninstall cuopt-thin-client - pip install --extra-index-url=https://pypi.nvidia.com libcuopt-cu12==25.10.* nvidia-cuda-runtime-cu12==12.8.* + pip install --extra-index-url=https://pypi.nvidia.com libcuopt-cu12==25.10.* nvidia-cuda-runtime-cu12==12.9.* Conda diff --git a/docs/cuopt/source/cuopt-python/quick-start.rst b/docs/cuopt/source/cuopt-python/quick-start.rst index f9f6f989a8..bdb94f7d7f 100644 --- a/docs/cuopt/source/cuopt-python/quick-start.rst +++ b/docs/cuopt/source/cuopt-python/quick-start.rst @@ -14,7 +14,7 @@ For CUDA 12.x: .. code-block:: bash - pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.10.* nvidia-cuda-runtime-cu12==12.8.* + pip install --extra-index-url=https://pypi.nvidia.com cuopt-cu12==25.10.* nvidia-cuda-runtime-cu12==12.9.* .. note:: @@ -41,19 +41,19 @@ NVIDIA cuOpt is also available as a container from Docker Hub: .. code-block:: bash - docker pull nvidia/cuopt:latest-cuda12.8-py3.12 + docker pull nvidia/cuopt:latest-cuda12.9-py3.12 .. note:: - The ``latest`` tag is the latest stable release of cuOpt. If you want to use a specific version, you can use the ``-cuda12.8-py3.12`` tag. For example, to use cuOpt 25.5.0, you can use the ``25.5.0-cuda12.8-py3.12`` tag. Please refer to `cuOpt dockerhub page `_ for the list of available tags. + The ``latest`` tag is the latest stable release of cuOpt. If you want to use a specific version, you can use the ``-cuda12.9-py3.12`` tag. For example, to use cuOpt 25.5.0, you can use the ``25.5.0-cuda12.9-py3.12`` tag. Please refer to `cuOpt dockerhub page `_ for the list of available tags. .. note:: - The nightly version of cuOpt is available as ``[VERSION]a-cuda12.8-py3.12`` tag. For example, to use cuOpt 25.8.0a, you can use the ``25.8.0a-cuda12.8-py3.12`` tag. + The nightly version of cuOpt is available as ``[VERSION]a-cuda12.9-py3.12`` tag. For example, to use cuOpt 25.8.0a, you can use the ``25.8.0a-cuda12.9-py3.12`` tag. The container includes both the Python API and self-hosted server components. To run the container: .. code-block:: bash - docker run --gpus all -it --rm nvidia/cuopt:latest-cuda12.8-py3.12 /bin/bash + docker run --gpus all -it --rm nvidia/cuopt:latest-cuda12.9-py3.12 /bin/bash This will start an interactive session with cuOpt pre-installed and ready to use. diff --git a/docs/cuopt/source/cuopt-server/quick-start.rst b/docs/cuopt/source/cuopt-server/quick-start.rst index 617a2c2e40..6084b73f50 100644 --- a/docs/cuopt/source/cuopt-server/quick-start.rst +++ b/docs/cuopt/source/cuopt-server/quick-start.rst @@ -12,7 +12,7 @@ For CUDA 12.x: .. code-block:: bash - pip install --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12==25.10.* cuopt-sh-client==25.10.* nvidia-cuda-runtime-cu12==12.8.* + pip install --extra-index-url=https://pypi.nvidia.com cuopt-server-cu12==25.10.* cuopt-sh-client==25.10.* nvidia-cuda-runtime-cu12==12.9.* .. note:: For development wheels which are available as nightlies, please update `--extra-index-url` to `https://pypi.anaconda.org/rapidsai-wheels-nightly/simple/`. @@ -37,19 +37,19 @@ NVIDIA cuOpt is also available as a container from Docker Hub: .. code-block:: bash - docker pull nvidia/cuopt:latest-cuda12.8-py3.12 + docker pull nvidia/cuopt:latest-cuda12.9-py3.12 .. note:: - The ``latest`` tag is the latest stable release of cuOpt. If you want to use a specific version, you can use the ``-cuda12.8-py3.12`` tag. For example, to use cuOpt 25.5.0, you can use the ``25.5.0-cuda12.8-py3.12`` tag. Please refer to `cuOpt dockerhub page `_ for the list of available tags. + The ``latest`` tag is the latest stable release of cuOpt. If you want to use a specific version, you can use the ``-cuda12.9-py3.12`` tag. For example, to use cuOpt 25.5.0, you can use the ``25.5.0-cuda12.9-py3.12`` tag. Please refer to `cuOpt dockerhub page `_ for the list of available tags. The container includes both the Python API and self-hosted server components. To run the container: .. code-block:: bash - docker run --gpus all -it --rm -p 8000:8000 -e CUOPT_SERVER_PORT=8000 nvidia/cuopt:latest-cuda12.8-py3.12 + docker run --gpus all -it --rm -p 8000:8000 -e CUOPT_SERVER_PORT=8000 nvidia/cuopt:latest-cuda12.9-py3.12 .. note:: - The nightly version of cuOpt is available as ``[VERSION]a-cuda12.8-py3.12`` tag. For example, to use cuOpt 25.8.0a, you can use the ``25.8.0a-cuda12.8-py3.12`` tag. + The nightly version of cuOpt is available as ``[VERSION]a-cuda12.9-py3.12`` tag. For example, to use cuOpt 25.8.0a, you can use the ``25.8.0a-cuda12.9-py3.12`` tag. .. note:: Make sure you have the NVIDIA Container Toolkit installed on your system to enable GPU support in containers. See the `installation guide `_ for details. diff --git a/docs/cuopt/source/system-requirements.rst b/docs/cuopt/source/system-requirements.rst index 132e4bc472..e7d963ae57 100644 --- a/docs/cuopt/source/system-requirements.rst +++ b/docs/cuopt/source/system-requirements.rst @@ -46,7 +46,7 @@ Dependencies are installed automatically when using the pip and Conda installati - CUDA 12.0 with Driver 525.60.13+ - CUDA 12.2 with Driver 535.86.10+ - CUDA 12.5 with Driver 555.42.06+ - - CUDA 12.8 with Driver 570.42.01+ + - CUDA 12.9 with Driver 570.42.01+ .. dropdown:: Recommended Requirements for Best Performance @@ -67,7 +67,7 @@ Dependencies are installed automatically when using the pip and Conda installati - 100+ GB free space * CUDA: - - 12.8 + - 12.9 * Latest NVIDIA drivers (570.42.01+) diff --git a/helmchart/cuopt-server/values.yaml b/helmchart/cuopt-server/values.yaml index 0b6b9746f7..79224fbc7e 100644 --- a/helmchart/cuopt-server/values.yaml +++ b/helmchart/cuopt-server/values.yaml @@ -7,7 +7,7 @@ replicaCount: 1 image: repository: nvidia/cuopt pullPolicy: IfNotPresent - tag: "25.10.0-cuda12.8-py3.12" + tag: "25.10.0-cuda12.9-py3.12" imagePullSecrets: [] nameOverride: "" From a5e168243b6799f3c09a705785a9e332f95db542 Mon Sep 17 00:00:00 2001 From: ahehn-nv Date: Tue, 26 Aug 2025 20:55:47 +0200 Subject: [PATCH 22/44] Performance tweak for dual_simplex/right_looking_lu (#315) Small performance optimization of `right_looking_lu()` and `right_looking_lu_row_permutation_only()`. Authors: - https://github.com/ahehn-nv - Ramakrishnap (https://github.com/rgsl888prabhu) - Chris Maes (https://github.com/chris-maes) Approvers: - Nicolas Blin (https://github.com/Kh4ster) URL: https://github.com/NVIDIA/cuopt/pull/315 --- cpp/src/dual_simplex/right_looking_lu.cpp | 49 +++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/cpp/src/dual_simplex/right_looking_lu.cpp b/cpp/src/dual_simplex/right_looking_lu.cpp index caf80ad111..2a919e690a 100644 --- a/cpp/src/dual_simplex/right_looking_lu.cpp +++ b/cpp/src/dual_simplex/right_looking_lu.cpp @@ -21,7 +21,6 @@ #include #include #include -#include namespace cuopt::linear_programming::dual_simplex { @@ -44,8 +43,8 @@ i_t initialize_degree_data(const csc_matrix_t& A, const std::vector& column_list, std::vector& Cdegree, std::vector& Rdegree, - std::vector>& col_count, - std::vector>& row_count) + std::vector>& col_count, + std::vector>& row_count) { const i_t n = column_list.size(); const i_t m = A.m; @@ -195,8 +194,8 @@ void initialize_max_in_row(const std::vector& first_in_row, template i_t markowitz_search(const std::vector& Cdegree, const std::vector& Rdegree, - const std::vector>& col_count, - const std::vector>& row_count, + const std::vector>& col_count, + const std::vector>& row_count, const std::vector& first_in_row, const std::vector& first_in_col, const std::vector& max_in_column, @@ -306,7 +305,7 @@ void update_Cdegree_and_col_count(i_t pivot_i, i_t pivot_j, const std::vector& first_in_row, std::vector& Cdegree, - std::vector>& col_count, + std::vector>& col_count, std::vector>& elements) { // Update Cdegree and col_count @@ -316,11 +315,13 @@ void update_Cdegree_and_col_count(i_t pivot_i, assert(entry->i == pivot_i); i_t cdeg = Cdegree[j]; assert(cdeg >= 0); - for (typename std::list::iterator it = col_count[cdeg].begin(); + for (typename std::vector::iterator it = col_count[cdeg].begin(); it != col_count[cdeg].end(); it++) { if (*it == j) { - col_count[cdeg].erase(it); + // Remove col j from col_count[cdeg] + std::swap(*it, col_count[cdeg].back()); + col_count[cdeg].pop_back(); break; } } @@ -336,7 +337,7 @@ void update_Rdegree_and_row_count(i_t pivot_i, i_t pivot_j, const std::vector& first_in_col, std::vector& Rdegree, - std::vector>& row_count, + std::vector>& row_count, std::vector>& elements) { // Update Rdegree and row_count @@ -345,11 +346,13 @@ void update_Rdegree_and_row_count(i_t pivot_i, const i_t i = entry->i; i_t rdeg = Rdegree[i]; assert(rdeg >= 0); - for (typename std::list::iterator it = row_count[rdeg].begin(); + for (typename std::vector::iterator it = row_count[rdeg].begin(); it != row_count[rdeg].end(); it++) { if (*it == i) { - row_count[rdeg].erase(it); + // Remove row i from row_count[rdeg] + std::swap(*it, row_count[rdeg].back()); + row_count[rdeg].pop_back(); break; } } @@ -375,8 +378,8 @@ void schur_complement(i_t pivot_i, std::vector& max_in_row, std::vector& Rdegree, std::vector& Cdegree, - std::vector>& row_count, - std::vector>& col_count, + std::vector>& row_count, + std::vector>& col_count, std::vector>& elements) { for (i_t p1 = first_in_col[pivot_j]; p1 != kNone; p1 = elements[p1].next_in_column) { @@ -452,11 +455,13 @@ void schur_complement(i_t pivot_i, } row_last_workspace[i] = fill_p; i_t rdeg = Rdegree[i]; // Rdgree must increase - for (typename std::list::iterator it = row_count[rdeg].begin(); + for (typename std::vector::iterator it = row_count[rdeg].begin(); it != row_count[rdeg].end(); it++) { if (*it == i) { - row_count[rdeg].erase(it); // Remove row i from row_count[rdeg] + // Remove row i from row_count[rdeg] + std::swap(*it, row_count[rdeg].back()); + row_count[rdeg].pop_back(); break; } } @@ -464,11 +469,13 @@ void schur_complement(i_t pivot_i, row_count[rdeg].push_back(i); // Add row i to row_count[rdeg] i_t cdeg = Cdegree[j]; // Cdegree must increase - for (typename std::list::iterator it = col_count[cdeg].begin(); + for (typename std::vector::iterator it = col_count[cdeg].begin(); it != col_count[cdeg].end(); it++) { if (*it == j) { - col_count[cdeg].erase(it); // Remove column j from col_count[cdeg] + // Remove col j from col_count[cdeg] + std::swap(*it, col_count[cdeg].back()); + col_count[cdeg].pop_back(); break; } } @@ -593,9 +600,9 @@ i_t right_looking_lu(const csc_matrix_t& A, std::vector Rdegree(n); // Rdegree[i] is the degree of row i std::vector Cdegree(n); // Cdegree[j] is the degree of column j - std::vector> col_count( + std::vector> col_count( n + 1); // col_count[nz] is a list of columns with nz nonzeros in the active submatrix - std::vector> row_count( + std::vector> row_count( n + 1); // row_count[nz] is a list of rows with nz nonzeros in the active submatrix const i_t Bnz = initialize_degree_data(A, column_list, Cdegree, Rdegree, col_count, row_count); @@ -931,9 +938,9 @@ i_t right_looking_lu_row_permutation_only(const csc_matrix_t& A, std::vector Rdegree(m); // Rdegree[i] is the degree of row i std::vector Cdegree(n); // Cdegree[j] is the degree of column j - std::vector> col_count( + std::vector> col_count( m + 1); // col_count[nz] is a list of columns with nz nonzeros in the active submatrix - std::vector> row_count( + std::vector> row_count( n + 1); // row_count[nz] is a list of rows with nz nonzeros in the active submatrix std::vector column_list(n); From 2f8dcd93e1c1610a6b8ac60032d1b62c403afc04 Mon Sep 17 00:00:00 2001 From: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> Date: Tue, 26 Aug 2025 22:26:07 -0500 Subject: [PATCH 23/44] Reduce hard-coded version usage in repo (#337) Pr reduces number of hard coded versions in repo and also fixes few issues. ## Issue closes #329 Authors: - Ramakrishnap (https://github.com/rgsl888prabhu) Approvers: - James Lamb (https://github.com/jameslamb) - Trevor McKay (https://github.com/tmckayus) - Hugo Linsenmaier (https://github.com/hlinsen) URL: https://github.com/NVIDIA/cuopt/pull/337 --- .pre-commit-config.yaml | 7 ++ ci/build_docs.sh | 3 + ci/release/update-version.sh | 49 ++-------- ci/utils/update_doc_versions.py | 89 +++++++++++++++++++ cmake/rapids_config.cmake | 15 ++++ cpp/CMakeLists.txt | 6 +- cpp/doxygen/Doxyfile | 2 +- cpp/libmps_parser/CMakeLists.txt | 2 +- python/cuopt/CMakeLists.txt | 8 +- python/cuopt/cuopt/__init__.py | 2 +- python/cuopt/cuopt/_version.py | 2 + .../cuopt/linear_programming/CMakeLists.txt | 6 +- python/cuopt_server/cuopt_server/__init__.py | 6 +- python/cuopt_server/cuopt_server/_version.py | 2 + .../cuopt_server/utils/data_definition.py | 6 +- .../linear_programming/data_definition.py | 4 +- .../utils/routing/data_definition.py | 4 +- python/libcuopt/CMakeLists.txt | 6 +- 18 files changed, 153 insertions(+), 66 deletions(-) create mode 100644 ci/utils/update_doc_versions.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9e1abedc8..d5ac1c0ac7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -97,6 +97,13 @@ repos: (?x)^( ^cpp/tests/utilities/cxxopts.hpp ) + - repo: local + hooks: + - id: update-versions + name: Update versions1.json + entry: python ci/utils/update_doc_versions.py + language: system + files: docs/cuopt/source/versions1.json default_language_version: diff --git a/ci/build_docs.sh b/ci/build_docs.sh index 3089358c97..127e390034 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -22,6 +22,9 @@ rapids-logger "Create test conda environment" ENV_YAML_DIR="$(mktemp -d)" +CUOPT_VERSION_MAJOR_MINOR="$(rapids-version-major-minor)" +export CUOPT_VERSION_MAJOR_MINOR + rapids-logger "Downloading artifacts from previous jobs" CPP_CHANNEL=$(rapids-download-conda-from-github cpp) PYTHON_CHANNEL=$(rapids-download-conda-from-github python) diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index 9c5f21a8ee..ea37fe83f7 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -57,57 +57,23 @@ DEPENDENCIES=( ) for DEP in "${DEPENDENCIES[@]}"; do - for FILE in dependencies.yaml conda/environments/*.yaml python/*/pyproject.toml; do - sed_runner "s/\(${DEP}==\)[0-9]\+\.[0-9]\+/\1${NEXT_SHORT_TAG_PEP440}/" "${FILE}" + for FILE in dependencies.yaml conda/environments/*.yaml; do + sed_runner "/-.* ${DEP}\(-cu[[:digit:]]\{2\}\)\{0,1\}\(\[.*\]\)\{0,1\}==/ s/==.*/==${NEXT_SHORT_TAG_PEP440}.*,>=0.0.0a0/g" "${FILE}" done for FILE in python/*/pyproject.toml; do - sed_runner "/-.* ${DEP}\(-cu[[:digit:]]\{2\}\)\{0,1\}\(\[.*\]\)\{0,1\}==/ s/==.*/==${NEXT_SHORT_TAG_PEP440}.*,>=0.0.0a0/g" "${FILE}" + sed_runner "/\"${DEP}==/ s/==.*\"/==${NEXT_SHORT_TAG_PEP440}.*,>=0.0.0a0\"/g" "${FILE}" done for FILE in docs/cuopt/source/*/quick-start.rst README.md; do - sed_runner "s/\(${DEP}==\)[0-9]\+\.[0-9]\+\.\\*/\1${NEXT_SHORT_TAG_PEP440}.\*/g" "${FILE}" - sed_runner "s/\(${DEP}=\)[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?[^ ]*/\1${NEXT_SHORT_TAG}.*/g" "${FILE}" - sed_runner "s/\(${DEP}:\)[0-9]\{2\}\.[0-9]\{1,2\}\.[0-9]\+\(-cuda[0-9]\+\.[0-9]\+-\)\(py[0-9]\+\)/\1${DOCKER_TAG}\2\3/g" "${FILE}" + sed_runner "/${DEP}\(-cu[[:digit:]]\{2\}\)\{0,1\}==/ s/==[0-9]\+\.[0-9]\+\.\*/==${NEXT_SHORT_TAG_PEP440}.\*/g" "${FILE}" + sed_runner "/${DEP}=/ s/=[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?[^ ]*/=${NEXT_SHORT_TAG}.*/g" "${FILE}" + sed_runner "/${DEP}:/ s/:[0-9]\{2\}\.[0-9]\{1,2\}\.[0-9]\+\(-cuda[0-9]\+\.[0-9]\+-\)\(py[0-9]\+\)/:${DOCKER_TAG}\1\2/g" "${FILE}" done done -# CMakeLists update -sed_runner 's/'"VERSION [0-9][0-9].[0-9][0-9].[0-9][0-9]"'/'"VERSION ${NEXT_FULL_TAG}"'/g' cpp/CMakeLists.txt -sed_runner 's/'"VERSION [0-9][0-9].[0-9][0-9].[0-9][0-9]"'/'"VERSION ${NEXT_FULL_TAG}"'/g' cpp/libmps_parser/CMakeLists.txt -sed_runner 's/'"DEPENDENT_LIB_MAJOR_VERSION \"[0-9][0-9]\""'/'"DEPENDENT_LIB_MAJOR_VERSION \"${NEXT_MAJOR}\""'/g' cpp/CMakeLists.txt -sed_runner 's/'"DEPENDENT_LIB_MINOR_VERSION \"[0-9][0-9]\""'/'"DEPENDENT_LIB_MINOR_VERSION \"${NEXT_MINOR}\""'/g' cpp/CMakeLists.txt - -# Server version update -sed_runner 's/'"\"version\": \"[0-9][0-9].[0-9][0-9]\""'/'"\"version\": \"${NEXT_SHORT_TAG}\""'/g' python/cuopt_server/cuopt_server/utils/data_definition.py -sed_runner 's/'"\"client_version\": \"[0-9][0-9].[0-9][0-9]\""'/'"\"client_version\": \"${NEXT_SHORT_TAG}\""'/g' python/cuopt_server/cuopt_server/utils/routing/data_definition.py -sed_runner 's/'"\"client_version\": \"[0-9][0-9].[0-9][0-9]\""'/'"\"client_version\": \"${NEXT_SHORT_TAG}\""'/g' python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py - -# Doc update -sed_runner 's/'"version = \"[0-9][0-9].[0-9][0-9]\""'/'"version = \"${NEXT_SHORT_TAG}\""'/g' docs/cuopt/source/conf.py -sed_runner 's/'"PROJECT_NUMBER = [0-9][0-9].[0-9][0-9]"'/'"PROJECT_NUMBER = ${NEXT_SHORT_TAG}"'/g' cpp/doxygen/Doxyfile - # Update project.json PROJECT_FILE="docs/cuopt/source/project.json" sed_runner 's/\("version": "\)[0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]"/\1'${NEXT_FULL_TAG}'"/g' "${PROJECT_FILE}" -# Update VERSIONS.json -VERSIONS_FILE="docs/cuopt/source/versions1.json" -# Only update if NEXT_FULL_TAG is not already present -if ! grep -q "\"version\": \"${NEXT_FULL_TAG}\"" "${VERSIONS_FILE}"; then - # Remove preferred and latest flags, but keep the version entry and URL - sed_runner '/"name": "latest",/d' "${VERSIONS_FILE}" - sed_runner '/"preferred": true,\?/d' "${VERSIONS_FILE}" - # Remove trailing comma after "url": ... in all version entries - sed_runner 's/\("url": "[^"]*"\),/\1/' "${VERSIONS_FILE}" - # Add new version entry with both preferred and latest flags - NEW_VERSION_ENTRY=' {\n "version": "'${NEXT_FULL_TAG}'",\n "url": "../'${NEXT_FULL_TAG}'/",\n "name": "latest",\n "preferred": true\n },' - sed_runner "/\[/a\\${NEW_VERSION_ENTRY}" "${VERSIONS_FILE}" -fi - -# RTD update -sed_runner "/^set(cuopt_version/ s/[0-9][0-9].[0-9][0-9].[0-9][0-9]/${NEXT_FULL_TAG}/g" python/cuopt/CMakeLists.txt -sed_runner "/^set(cuopt_version/ s/[0-9][0-9].[0-9][0-9].[0-9][0-9]/${NEXT_FULL_TAG}/g" python/cuopt/cuopt/linear_programming/CMakeLists.txt -sed_runner "/^set(cuopt_version/ s/[0-9][0-9].[0-9][0-9].[0-9][0-9]/${NEXT_FULL_TAG}/g" python/libcuopt/CMakeLists.txt - # Update nightly sed_runner 's/'"cuopt_version: \"[0-9][0-9].[0-9][0-9]\""'/'"cuopt_version: \"${NEXT_SHORT_TAG}\""'/g' .github/workflows/nightly.yaml @@ -121,6 +87,3 @@ for FILE in .github/workflows/*.yaml; do # CI image tags of the form {rapids_version}-{something} sed_runner "s/:[0-9]*\\.[0-9]*-/:${NEXT_SHORT_TAG}-/g" "${FILE}" done - -# PYTHON for RAPIDS -sed_runner "/DOWNLOAD.*rapids-cmake/ s/branch-[0-9][0-9].[0-9][0-9]/branch-${NEXT_SHORT_TAG}/g" python/cuopt/CMakeLists.txt diff --git a/ci/utils/update_doc_versions.py b/ci/utils/update_doc_versions.py new file mode 100644 index 0000000000..8c2a1e19a3 --- /dev/null +++ b/ci/utils/update_doc_versions.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Script to update versions1.json with new version from VERSION file. +This script reads the current version from the VERSION file and adds it to versions1.json +if it doesn't already exist, setting it as the latest and preferred version. +""" + +import json +import os +import sys +from pathlib import Path + + +def update_versions(version_file_path, versions_file_path): + """Update the versions list with the new version.""" + # Read VERSION file + with open(version_file_path, "r") as f: + version = f.read().strip() + if not version: + raise ValueError("VERSION file is empty") + + # Read versions1.json file + with open(versions_file_path, "r") as f: + versions = json.load(f) + + # Check if version already exists + for version_entry in versions: + if version_entry.get("version") == version: + print(f"Version {version} already exists in versions1.json") + return False + + # Remove "latest" and "preferred" from existing entries + for version_entry in versions: + if version_entry.get("name") == "latest": + version_entry.pop("name", None) + if version_entry.get("preferred"): + version_entry.pop("preferred", None) + + # Create new entry for the current version + new_entry = { + "version": version, + "url": f"../{version}/", + "name": "latest", + "preferred": True, + } + + # Add new entry at the beginning (most recent first) + versions.insert(0, new_entry) + + # Write updated versions back to file + with open(versions_file_path, "w") as f: + json.dump(versions, f, indent=2) + + return True + + +def main(): + """Main function to update versions1.json.""" + # Get the repository root directory (assuming script is run from repo root) + repo_root = Path.cwd() + + # Hard-coded file paths + version_file_path = repo_root / "VERSION" + versions_file_path = ( + repo_root / "docs" / "cuopt" / "source" / "versions1.json" + ) + + # Update versions + update_versions(version_file_path, versions_file_path) + + +if __name__ == "__main__": + main() diff --git a/cmake/rapids_config.cmake b/cmake/rapids_config.cmake index 9b361d59fc..898680fe26 100644 --- a/cmake/rapids_config.cmake +++ b/cmake/rapids_config.cmake @@ -28,3 +28,18 @@ endif() set(rapids-cmake-version "${RAPIDS_VERSION_MAJOR_MINOR}") include("${CMAKE_CURRENT_LIST_DIR}/RAPIDS.cmake") + +file(READ "${CMAKE_CURRENT_LIST_DIR}/../VERSION" _cuopt_version) +if(_cuopt_version MATCHES [[^([0-9][0-9])\.([0-9][0-9])\.([0-9][0-9])]]) + set(CUOPT_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CUOPT_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CUOPT_VERSION_PATCH "${CMAKE_MATCH_3}") + set(CUOPT_VERSION_MAJOR_MINOR "${CUOPT_VERSION_MAJOR}.${CUOPT_VERSION_MINOR}") + set(CUOPT_VERSION "${CUOPT_VERSION_MAJOR}.${CUOPT_VERSION_MINOR}.${CUOPT_VERSION_PATCH}") +else() + string(REPLACE "\n" "\n " _cuopt_version_formatted " ${_cuopt_version}") + message( + FATAL_ERROR + "Could not determine cuOpt version. Contents of VERSION file:\n${_cuopt_version_formatted}" + ) +endif() diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 290016f902..76c856e0e5 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -32,12 +32,12 @@ rapids_cuda_init_architectures(CUOPT) project( CUOPT - VERSION 25.10.00 + VERSION "${CUOPT_VERSION}" LANGUAGES CXX CUDA C ) -set(DEPENDENT_LIB_MAJOR_VERSION "25") -set(DEPENDENT_LIB_MINOR_VERSION "10") +set(DEPENDENT_LIB_MAJOR_VERSION "${RAPIDS_VERSION_MAJOR}") +set(DEPENDENT_LIB_MINOR_VERSION "${RAPIDS_VERSION_MINOR}") rapids_cmake_write_version_file(include/cuopt/version_config.hpp) # ################################################################################################## diff --git a/cpp/doxygen/Doxyfile b/cpp/doxygen/Doxyfile index 12ae21c19a..cf86936a9c 100644 --- a/cpp/doxygen/Doxyfile +++ b/cpp/doxygen/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "libcuopt" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 25.10 +PROJECT_NUMBER = "$(CUOPT_VERSION_MAJOR_MINOR)" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/cpp/libmps_parser/CMakeLists.txt b/cpp/libmps_parser/CMakeLists.txt index d6b87f03fa..33f0a7b581 100644 --- a/cpp/libmps_parser/CMakeLists.txt +++ b/cpp/libmps_parser/CMakeLists.txt @@ -22,7 +22,7 @@ include(rapids-find) project( MPS_PARSER - VERSION 25.10.00 + VERSION "${CUOPT_VERSION}" LANGUAGES CXX ) diff --git a/python/cuopt/CMakeLists.txt b/python/cuopt/CMakeLists.txt index 097082d709..a6ae668e7d 100644 --- a/python/cuopt/CMakeLists.txt +++ b/python/cuopt/CMakeLists.txt @@ -15,15 +15,13 @@ cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) -set(cuopt_version 25.10.00) - include(../../cmake/rapids_config.cmake) include(rapids-cuda) rapids_cuda_init_architectures(CUOPT) project( cuopt-python - VERSION ${cuopt_version} + VERSION "${CUOPT_VERSION}" LANGUAGES # TODO: Building Python extension modules via the python_extension_module requires the C # language to be enabled here. The test project that is built in scikit-build to verify # various linking options for the python library is hardcoded to build with C, so until @@ -31,8 +29,8 @@ project( C CXX CUDA) -find_package(cuopt ${cuopt_version}) -find_package(mps_parser ${cuopt_version}) +find_package(cuopt "${CUOPT_VERSION}") +find_package(mps_parser "${CUOPT_VERSION}") include(rapids-cython-core) rapids_cython_init() diff --git a/python/cuopt/cuopt/__init__.py b/python/cuopt/cuopt/__init__.py index c12a888281..ab52891e3c 100644 --- a/python/cuopt/cuopt/__init__.py +++ b/python/cuopt/cuopt/__init__.py @@ -22,4 +22,4 @@ del libcuopt from cuopt import linear_programming, routing -from cuopt._version import __git_commit__, __version__ +from cuopt._version import __git_commit__, __version__, __version_major_minor__ diff --git a/python/cuopt/cuopt/_version.py b/python/cuopt/cuopt/_version.py index 6d6f1fa097..a89da8a08d 100644 --- a/python/cuopt/cuopt/_version.py +++ b/python/cuopt/cuopt/_version.py @@ -19,3 +19,5 @@ importlib.resources.files("cuopt").joinpath("VERSION").read_text().strip() ) __git_commit__ = "" + +__version_major_minor__ = ".".join(__version__.split(".")[:2]) diff --git a/python/cuopt/cuopt/linear_programming/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/CMakeLists.txt index c31ed89bef..1f46f31826 100644 --- a/python/cuopt/cuopt/linear_programming/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/CMakeLists.txt @@ -15,13 +15,11 @@ cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) -set(cuopt_version 25.10.00) - include(../../../../cmake/rapids_config.cmake) project( mpsparser-python - VERSION ${cuopt_version} + VERSION "${CUOPT_VERSION}" LANGUAGES # TODO: Building Python extension modules via the python_extension_module requires the C # language to be enabled here. The test project that is built in scikit-build to verify # various linking options for the python library is hardcoded to build with C, so until @@ -34,7 +32,7 @@ option(CUOPT_BUILD_WHEELS "Whether this build is generating a Python wheel." ON) # If the user requested it we attempt to find MPS Parser. if(FIND_MPS_PARSER_CPP) - find_package(mps_parser ${cuopt_version}) + find_package(mps_parser "${CUOPT_VERSION}") else() set(mps_parser_FOUND OFF) endif() diff --git a/python/cuopt_server/cuopt_server/__init__.py b/python/cuopt_server/cuopt_server/__init__.py index c8477dae4f..55a82de16f 100644 --- a/python/cuopt_server/cuopt_server/__init__.py +++ b/python/cuopt_server/cuopt_server/__init__.py @@ -14,4 +14,8 @@ # limitations under the License. from cuopt_server import cuopt_service -from cuopt_server._version import __git_commit__, __version__ +from cuopt_server._version import ( + __git_commit__, + __version__, + __version_major_minor__, +) diff --git a/python/cuopt_server/cuopt_server/_version.py b/python/cuopt_server/cuopt_server/_version.py index ba27ac7b05..d0c1ac4afc 100644 --- a/python/cuopt_server/cuopt_server/_version.py +++ b/python/cuopt_server/cuopt_server/_version.py @@ -22,3 +22,5 @@ .strip() ) __git_commit__ = "" + +__version_major_minor__ = ".".join(__version__.split(".")[:2]) diff --git a/python/cuopt_server/cuopt_server/utils/data_definition.py b/python/cuopt_server/cuopt_server/utils/data_definition.py index 4d4df04346..4fb5d8d2ce 100644 --- a/python/cuopt_server/cuopt_server/utils/data_definition.py +++ b/python/cuopt_server/cuopt_server/utils/data_definition.py @@ -23,6 +23,7 @@ import jsonref from pydantic import BaseModel, Extra, Field +from .._version import __version_major_minor__ from .linear_programming.data_definition import ( # noqa IncumbentSolution, LPData, @@ -343,7 +344,10 @@ class LogResponseModel(StrictModel): "application/json": { "examples": { "Healthy response": { - "value": {"status": "RUNNING", "version": "25.10"} + "value": { + "status": "RUNNING", + "version": __version_major_minor__, + } } } } diff --git a/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py index 5c5ee7e6ee..30dcf8695f 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py @@ -22,6 +22,8 @@ from pydantic import BaseModel, Extra, Field, PlainValidator from typing_extensions import Annotated +from ..._version import __version_major_minor__ + # INPUT DATA DEFINITIONS @@ -853,7 +855,7 @@ class IncumbentSolution(StrictModel): managed_lp_example_data = { "action": "cuOpt_LP", "data": lp_example_data, - "client_version": "25.10", + "client_version": __version_major_minor__, } # cut and pasted from actual run of LP example data. diff --git a/python/cuopt_server/cuopt_server/utils/routing/data_definition.py b/python/cuopt_server/cuopt_server/utils/routing/data_definition.py index 03bc63f1ef..530af4712d 100644 --- a/python/cuopt_server/cuopt_server/utils/routing/data_definition.py +++ b/python/cuopt_server/cuopt_server/utils/routing/data_definition.py @@ -21,6 +21,8 @@ import jsonref from pydantic import BaseModel, Extra, Field, RootModel, root_validator +from ..._version import __version_major_minor__ + class LocationTypeEnum(str, Enum): Depot = "Depot" @@ -1026,7 +1028,7 @@ class InFeasibleSolve(StrictModel): managed_vrp_example_data = { "action": "cuOpt_OptimizedRouting", "data": vrp_example_data, - "client_version": "25.10", + "client_version": __version_major_minor__, } # cut and pasted from actual run of VRP example data. diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index b997708876..85b7c98513 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -15,21 +15,19 @@ cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) -set(cuopt_version 25.10.00) - include(../../cmake/rapids_config.cmake) include(rapids-cuda) rapids_cuda_init_architectures(libcuopt-python) project( libcuopt-python - VERSION "${cuopt_version}" + VERSION "${CUOPT_VERSION}" LANGUAGES CXX CUDA ) # Check if cuopt is already available. If so, it is the user's responsibility to ensure that the # CMake package is also available at build time of the Python cuopt package. -find_package(cuopt "${cuopt_version}") +find_package(cuopt "${CUOPT_VERSION}") if(cuopt_FOUND) return() From defb9477e493d0ad1554b834900cbe110690fc0b Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 27 Aug 2025 13:13:17 +0200 Subject: [PATCH 24/44] Moved all shared variables to be member variables. --- cpp/src/dual_simplex/branch_and_bound.cpp | 622 +++++++++++----------- cpp/src/dual_simplex/branch_and_bound.hpp | 74 +-- cpp/src/dual_simplex/pseudo_costs.hpp | 29 +- 3 files changed, 363 insertions(+), 362 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index c5ea2922f2..b37d032d78 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -193,18 +193,18 @@ dual::status_t convert_lp_status_to_dual_status(lp_status_t status) template f_t branch_and_bound_t::get_upper_bound() { - mutex_upper.lock(); + mutex_upper_.lock(); const f_t upper_bound = upper_bound_; - mutex_upper.unlock(); + mutex_upper_.unlock(); return upper_bound; } template f_t branch_and_bound_t::get_lower_bound() { - mutex_lower.lock(); + mutex_lower_.lock(); const f_t lower_bound = lower_bound_; - mutex_lower.unlock(); + mutex_lower_.unlock(); return lower_bound; } @@ -242,30 +242,30 @@ std::string user_mip_gap(f_t obj_value, f_t lower_bound) template void branch_and_bound_t::set_new_solution(const std::vector& solution) { - if (solution.size() != original_problem.num_cols) { - settings.log.printf( - "Solution size mismatch %ld %d\n", solution.size(), original_problem.num_cols); + if (solution.size() != original_problem_.num_cols) { + settings_.log.printf( + "Solution size mismatch %ld %d\n", solution.size(), original_problem_.num_cols); } std::vector crushed_solution; crush_primal_solution( - original_problem, original_lp, solution, new_slacks, crushed_solution); - f_t obj = compute_objective(original_lp, crushed_solution); + original_problem_, original_lp_, solution, new_slacks_, crushed_solution); + f_t obj = compute_objective(original_lp_, crushed_solution); bool is_feasible = false; bool attempt_repair = false; - mutex_upper.lock(); + mutex_upper_.lock(); if (obj < upper_bound_) { f_t primal_err; f_t bound_err; i_t num_fractional; is_feasible = check_guess( - original_lp, settings, var_types, crushed_solution, primal_err, bound_err, num_fractional); + original_lp_, settings_, var_types_, crushed_solution, primal_err, bound_err, num_fractional); if (is_feasible) { upper_bound_ = obj; } else { attempt_repair = true; constexpr bool verbose = false; if (verbose) { - settings.log.printf( + settings_.log.printf( "Injected solution infeasible. Constraint error %e bound error %e integer infeasible " "%d\n", primal_err, @@ -274,68 +274,66 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu } } } - mutex_upper.unlock(); + mutex_upper_.unlock(); if (is_feasible) { - mutex_lower.lock(); + mutex_lower_.lock(); f_t lower_bound = lower_bound_; - mutex_lower.unlock(); - mutex_branching.lock(); + mutex_lower_.unlock(); + mutex_branching_.lock(); bool currently_branching = currently_branching_; - mutex_branching.unlock(); + mutex_branching_.unlock(); if (currently_branching) { - settings.log.printf( + settings_.log.printf( "H %+13.6e %+10.6e %s %9.2f\n", - compute_user_objective(original_lp, obj), - compute_user_objective(original_lp, lower_bound), - user_mip_gap(compute_user_objective(original_lp, obj), - compute_user_objective(original_lp, lower_bound)) + compute_user_objective(original_lp_, obj), + compute_user_objective(original_lp_, lower_bound), + user_mip_gap(compute_user_objective(original_lp_, obj), + compute_user_objective(original_lp_, lower_bound)) .c_str(), toc(stats_.start_time)); } else { - settings.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", - compute_user_objective(original_lp, obj), - toc(stats_.start_time)); + settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", + compute_user_objective(original_lp_, obj), + toc(stats_.start_time)); } } if (attempt_repair) { - mutex_repair.lock(); - repair_queue.push_back(crushed_solution); - mutex_repair.unlock(); + mutex_repair_.lock(); + repair_queue_.push_back(crushed_solution); + mutex_repair_.unlock(); } } template -bool branch_and_bound_t::repair_solution( - const std::vector& root_vstatus, - const std::vector& edge_norms, - const std::vector& potential_solution, - f_t& repaired_obj, - std::vector& repaired_solution) const +bool branch_and_bound_t::repair_solution(const std::vector& edge_norms, + const std::vector& potential_solution, + f_t& repaired_obj, + std::vector& repaired_solution) const { bool feasible = false; repaired_obj = std::numeric_limits::quiet_NaN(); - i_t n = original_lp.num_cols; + i_t n = original_lp_.num_cols; assert(potential_solution.size() == n); - lp_problem_t repair_lp = original_lp; + lp_problem_t repair_lp = original_lp_; // Fix integer variables for (i_t j = 0; j < n; ++j) { - if (var_types[j] == variable_type_t::INTEGER) { + if (var_types_[j] == variable_type_t::INTEGER) { const f_t fixed_val = std::round(potential_solution[j]); repair_lp.lower[j] = fixed_val; repair_lp.upper[j] = fixed_val; } } - lp_solution_t lp_solution(original_lp.num_rows, original_lp.num_cols); + lp_solution_t lp_solution(original_lp_.num_rows, original_lp_.num_cols); i_t iter = 0; f_t lp_start_time = tic(); - simplex_solver_settings_t lp_settings = settings; - std::vector vstatus = root_vstatus; + simplex_solver_settings_t lp_settings = settings_; + std::vector vstatus = root_vstatus_; lp_settings.set_log(false); lp_settings.inside_mip = true; std::vector leaf_edge_norms = edge_norms; @@ -348,12 +346,17 @@ bool branch_and_bound_t::repair_solution( f_t primal_error; f_t bound_error; i_t num_fractional; - feasible = check_guess( - original_lp, settings, var_types, lp_solution.x, primal_error, bound_error, num_fractional); - repaired_obj = compute_objective(original_lp, repaired_solution); + feasible = check_guess(original_lp_, + settings_, + var_types_, + lp_solution.x, + primal_error, + bound_error, + num_fractional); + repaired_obj = compute_objective(original_lp_, repaired_solution); constexpr bool verbose = false; if (verbose) { - settings.log.printf( + settings_.log.printf( "After repair: feasible %d primal error %e bound error %e fractional %d. Objective %e\n", feasible, primal_error, @@ -370,23 +373,27 @@ template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings) - : original_problem(user_problem), settings(solver_settings), original_lp(1, 1, 1), incumbent_(1) + : original_problem_(user_problem), + settings_(solver_settings), + original_lp_(1, 1, 1), + incumbent_(1), + root_relax_soln_(1, 1) { stats_.start_time = tic(); - convert_user_problem(original_problem, settings, original_lp, new_slacks); - full_variable_types(original_problem, original_lp, var_types); + convert_user_problem(original_problem_, settings_, original_lp_, new_slacks_); + full_variable_types(original_problem_, original_lp_, var_types_); - mutex_upper.lock(); + mutex_upper_.lock(); upper_bound_ = inf; - mutex_upper.unlock(); + mutex_upper_.unlock(); - mutex_lower.lock(); + mutex_lower_.lock(); lower_bound_ = -inf; - mutex_lower.unlock(); + mutex_lower_.unlock(); - mutex_branching.lock(); + mutex_branching_.lock(); currently_branching_ = false; - mutex_branching.unlock(); + mutex_branching_.unlock(); } template @@ -395,46 +402,46 @@ void branch_and_bound_t::repair_heuristic_solutions(const f_t& lower_b { // Check if there are any solutions to repair std::vector> to_repair; - mutex_repair.lock(); - if (repair_queue.size() > 0) { - to_repair = repair_queue; - repair_queue.clear(); + mutex_repair_.lock(); + if (repair_queue_.size() > 0) { + to_repair = repair_queue_; + repair_queue_.clear(); } - mutex_repair.unlock(); + mutex_repair_.unlock(); if (to_repair.size() > 0) { - settings.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); + settings_.log.debug("Attempting to repair %ld injected solutions\n", to_repair.size()); for (const std::vector& potential_solution : to_repair) { std::vector repaired_solution; f_t repaired_obj; - bool is_feasible = repair_solution( - root_vstatus, edge_norms, potential_solution, repaired_obj, repaired_solution); + bool is_feasible = + repair_solution(edge_norms_, potential_solution, repaired_obj, repaired_solution); if (is_feasible) { - mutex_upper.lock(); + mutex_upper_.lock(); if (repaired_obj < upper_bound_) { upper_bound_ = repaired_obj; incumbent_.set_incumbent_solution(repaired_obj, repaired_solution); - f_t obj = compute_user_objective(original_lp, repaired_obj); - f_t lower = compute_user_objective(original_lp, lower_bound); + f_t obj = compute_user_objective(original_lp_, repaired_obj); + f_t lower = compute_user_objective(original_lp_, lower_bound); std::string gap = user_mip_gap(obj, lower); - settings.log.printf( + settings_.log.printf( "H %+13.6e %+10.6e %s %9.2f\n", obj, lower, gap.c_str(), toc(stats_.start_time)); - if (settings.solution_callback != nullptr) { + if (settings_.solution_callback != nullptr) { std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, repaired_solution, original_x); - settings.solution_callback(original_x, repaired_obj); + uncrush_primal_solution(original_problem_, original_lp_, repaired_solution, original_x); + settings_.solution_callback(original_x, repaired_obj); } } - mutex_upper.unlock(); + mutex_upper_.unlock(); } } } @@ -448,66 +455,64 @@ void branch_and_bound_t::branch(mip_node_t* parent_node, { // down child std::unique_ptr> down_child = std::make_unique>( - original_lp, parent_node, ++stats_.num_nodes, branch_var, 0, branch_var_val, parent_vstatus); + original_lp_, parent_node, ++stats_.num_nodes, branch_var, 0, branch_var_val, parent_vstatus); - graphviz_edge(settings, parent_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); + graphviz_edge( + settings_, parent_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); // up child std::unique_ptr> up_child = std::make_unique>( - original_lp, parent_node, ++stats_.num_nodes, branch_var, 1, branch_var_val, parent_vstatus); + original_lp_, parent_node, ++stats_.num_nodes, branch_var, 1, branch_var_val, parent_vstatus); - graphviz_edge(settings, parent_node, up_child.get(), branch_var, 1, std::ceil(branch_var_val)); + graphviz_edge(settings_, parent_node, up_child.get(), branch_var, 1, std::ceil(branch_var_val)); - assert(parent_vstatus.size() == original_lp.num_cols); + assert(parent_vstatus.size() == original_lp_.num_cols); parent_node->add_children(std::move(down_child), std::move(up_child)); // child pointers moved into the tree } template -mip_status_t branch_and_bound_t::solve_root_relaxation( - f_t& root_objective, - lp_solution_t& root_relax_soln, - std::vector& root_vstatus) +mip_status_t branch_and_bound_t::solve_root_relaxation() { - settings.log.printf("Solving LP root relaxation\n"); - simplex_solver_settings_t lp_settings = settings; + settings_.log.printf("Solving LP root relaxation\n"); + simplex_solver_settings_t lp_settings = settings_; lp_settings.inside_mip = 1; lp_status_t root_status = solve_linear_program_advanced( - original_lp, stats_.start_time, lp_settings, root_relax_soln, root_vstatus, edge_norms); + original_lp_, stats_.start_time, lp_settings, root_relax_soln_, root_vstatus_, edge_norms_); stats_.total_lp_solve_time = toc(stats_.start_time); - assert(root_vstatus.size() == original_lp.num_cols); + assert(root_vstatus_.size() == original_lp_.num_cols); if (root_status == lp_status_t::INFEASIBLE) { - settings.log.printf("MIP Infeasible\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); + settings_.log.printf("MIP Infeasible\n"); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); } return mip_status_t::INFEASIBLE; } if (root_status == lp_status_t::UNBOUNDED) { - settings.log.printf("MIP Unbounded\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); + settings_.log.printf("MIP Unbounded\n"); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); } return mip_status_t::UNBOUNDED; } if (root_status == lp_status_t::TIME_LIMIT) { - settings.log.printf("Hit time limit\n"); + settings_.log.printf("Hit time limit\n"); return mip_status_t::TIME_LIMIT; } - set_uninitialized_steepest_edge_norms(original_lp.num_cols, edge_norms); + set_uninitialized_steepest_edge_norms(original_lp_.num_cols, edge_norms_); std::vector fractional; - root_objective = compute_objective(original_lp, root_relax_soln.x); - if (settings.set_simplex_solution_callback != nullptr) { + root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + if (settings_.set_simplex_solution_callback != nullptr) { std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, root_relax_soln.x, original_x); - settings.set_simplex_solution_callback(original_x, - compute_user_objective(original_lp, root_objective)); + uncrush_primal_solution(original_problem_, original_lp_, root_relax_soln_.x, original_x); + settings_.set_simplex_solution_callback(original_x, + compute_user_objective(original_lp_, root_objective_)); } - mutex_lower.lock(); - lower_bound_ = root_objective; - mutex_lower.unlock(); + mutex_lower_.lock(); + lower_bound_ = root_objective_; + mutex_lower_.unlock(); return mip_status_t::UNSET; } @@ -518,7 +523,8 @@ mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* n f_t upper_bound, f_t lower_bound, i_t nodes_explored, - i_t unexplored_nodes) + i_t unexplored_nodes, + pseudo_costs_t& pc) { logger_t log; log.log = false; @@ -527,17 +533,17 @@ mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* n lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp.lower; - leaf_problem.upper = original_lp.upper; + leaf_problem.lower = original_lp_.lower; + leaf_problem.upper = original_lp_.upper; node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper); i_t node_iter = 0; assert(leaf_vstatus.size() == leaf_problem.num_cols); f_t lp_start_time = tic(); - std::vector leaf_edge_norms = edge_norms; // = node.steepest_edge_norms; - simplex_solver_settings_t lp_settings = settings; + std::vector leaf_edge_norms = edge_norms_; // = node.steepest_edge_norms; + simplex_solver_settings_t lp_settings = settings_; lp_settings.set_log(false); - lp_settings.cut_off = upper_bound + settings.dual_tol; + lp_settings.cut_off = upper_bound + settings_.dual_tol; lp_settings.inside_mip = 2; dual::status_t lp_status = dual_phase2(2, 0, @@ -549,23 +555,23 @@ mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* n node_iter, leaf_edge_norms); if (lp_status == dual::status_t::NUMERICAL) { - settings.log.printf("Numerical issue node %d. Resolving from scratch.\n", - stats_.nodes_explored.load()); + settings_.log.printf("Numerical issue node %d. Resolving from scratch.\n", + stats_.nodes_explored.load()); lp_status_t second_status = solve_linear_program_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } - mutex_stats.lock(); + mutex_stats_.lock(); stats_.total_lp_solve_time += toc(lp_start_time); stats_.total_lp_iters += node_iter; - mutex_stats.unlock(); + mutex_stats_.unlock(); if (lp_status == dual::status_t::DUAL_UNBOUNDED) { node_ptr->lower_bound = inf; std::vector*> stack; node_ptr->set_status(node_status_t::INFEASIBLE, stack); - graphviz_node(settings, node_ptr, "infeasible", 0.0); + graphviz_node(settings_, node_ptr, "infeasible", 0.0); remove_fathomed_nodes(stack); // Node was infeasible. Do not branch } else if (lp_status == dual::status_t::CUTOFF) { @@ -573,20 +579,18 @@ mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* n std::vector*> stack; node_ptr->set_status(node_status_t::FATHOMED, stack); f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "cut off", leaf_objective); + graphviz_node(settings_, node_ptr, "cut off", leaf_objective); remove_fathomed_nodes(stack); // Node was cut off. Do not branch } else if (lp_status == dual::status_t::OPTIMAL) { // LP was feasible std::vector fractional; const i_t leaf_fractional = - fractional_variables(settings, leaf_solution.x, var_types, fractional); + fractional_variables(settings_, leaf_solution.x, var_types_, fractional); f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); - graphviz_node(settings, node_ptr, "lower bound", leaf_objective); + graphviz_node(settings_, node_ptr, "lower bound", leaf_objective); - mutex_pseudocosts.lock(); pc.update_pseudo_costs(node_ptr, leaf_objective); - mutex_pseudocosts.unlock(); node_ptr->lower_bound = leaf_objective; @@ -594,64 +598,63 @@ mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* n if (leaf_fractional == 0) { bool send_solution = false; - mutex_upper.lock(); + mutex_upper_.lock(); if (leaf_objective < upper_bound_) { incumbent_.set_incumbent_solution(leaf_objective, leaf_solution.x); upper_bound_ = leaf_objective; gap = upper_bound_ - lower_bound; - f_t obj = compute_user_objective(original_lp, upper_bound_); - f_t lower = compute_user_objective(original_lp, lower_bound); - settings.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - unexplored_nodes, - obj, - lower, - node_ptr->depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(stats_.start_time)); + f_t obj = compute_user_objective(original_lp_, upper_bound_); + f_t lower = compute_user_objective(original_lp_, lower_bound); + settings_.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + unexplored_nodes, + obj, + lower, + node_ptr->depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(stats_.start_time)); send_solution = true; } - mutex_upper.unlock(); + mutex_upper_.unlock(); - if (send_solution && settings.solution_callback != nullptr) { + if (send_solution && settings_.solution_callback != nullptr) { std::vector original_x; - uncrush_primal_solution(original_problem, original_lp, incumbent_.x, original_x); - settings.solution_callback(original_x, upper_bound_); + uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, original_x); + settings_.solution_callback(original_x, upper_bound_); } if (send_solution) { - mutex_gap.lock(); + mutex_gap_.lock(); gap_ = gap; - mutex_gap.unlock(); + mutex_gap_.unlock(); } - graphviz_node(settings, node_ptr, "integer feasible", leaf_objective); + graphviz_node(settings_, node_ptr, "integer feasible", leaf_objective); std::vector*> stack; node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); remove_fathomed_nodes(stack); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - mutex_pseudocosts.lock(); const i_t branch_var = pc.variable_selection( fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); - mutex_pseudocosts.unlock(); assert(leaf_vstatus.size() == leaf_problem.num_cols); branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); node_ptr->set_status(node_status_t::HAS_CHILDREN); } else { - graphviz_node(settings, node_ptr, "fathomed", leaf_objective); + graphviz_node(settings_, node_ptr, "fathomed", leaf_objective); std::vector*> stack; node_ptr->set_status(node_status_t::FATHOMED, stack); remove_fathomed_nodes(stack); } } else { - graphviz_node(settings, node_ptr, "numerical", 0.0); - settings.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", lp_status); + graphviz_node(settings_, node_ptr, "numerical", 0.0); + settings_.log.printf("Encountered LP status %d. This indicates a numerical issue.\n", + lp_status); return mip_status_t::NUMERICAL; } @@ -659,39 +662,9 @@ mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* n } template -void branch_and_bound_t::report(f_t last_log, - i_t nodes_explored, - i_t nodes_unexplored, - f_t gap, - f_t upper_bound, - f_t lower_bound, - i_t leaf_depth) -{ - f_t now = toc(stats_.start_time); - f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); - if ((nodes_explored % 1000 == 0 || gap < 10 * settings.absolute_mip_gap_tol || - nodes_explored < 1000) && - (time_since_log >= 1) || - (time_since_log > 60) || now > settings.time_limit) { - settings.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - nodes_unexplored, - compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound), - leaf_depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp, upper_bound), - compute_user_objective(original_lp, lower_bound)) - .c_str(), - now); - last_log = tic(); - } -} - -template -mip_status_t branch_and_bound_t::explore_tree(mip_node_t* start_node, - f_t root_objective, - mip_solution_t& solution) +mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, + mip_solution_t& solution, + pseudo_costs_t pc) { mip_status_t status = mip_status_t::UNSET; logger_t log; @@ -705,19 +678,25 @@ mip_status_t branch_and_bound_t::explore_tree(mip_node_t* st std::priority_queue*, std::vector*>, decltype(compare)> heap(compare); - heap.push(start_node->get_down_child()); // the heap does not own the unique_ptr the tree does - heap.push(start_node->get_up_child()); // the heap does not own the unqiue_ptr the tree does + mip_node_t root_node(root_objective_, root_vstatus_); + graphviz_node(settings_, &root_node, "lower bound", root_objective_); + + branch(&root_node, branch_var, root_relax_soln_.x[branch_var], root_vstatus_); + + // the stack does not own the unique_ptr the tree does + heap.push(root_node.get_down_child()); + heap.push(root_node.get_up_child()); // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp; + lp_problem_t leaf_problem = original_lp_; f_t lower_bound = get_lower_bound(); f_t gap = get_upper_bound() - lower_bound; f_t last_log = 0; i_t nodes_explored = 0; - while (gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && + while (gap > settings_.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) > settings_.relative_mip_gap_tol && heap.size() > 0) { repair_heuristic_solutions(lower_bound, solution); @@ -732,28 +711,46 @@ mip_status_t branch_and_bound_t::explore_tree(mip_node_t* st // upper bound std::vector*> stack; node_ptr->set_status(node_status_t::FATHOMED, stack); - graphviz_node(settings, node_ptr, "cutoff", node_ptr->lower_bound); + graphviz_node(settings_, node_ptr, "cutoff", node_ptr->lower_bound); remove_fathomed_nodes(stack); continue; } - mutex_lower.lock(); + mutex_lower_.lock(); lower_bound = lower_bound_ = node_ptr->lower_bound; - mutex_lower.unlock(); + mutex_lower_.unlock(); - mutex_gap.lock(); + mutex_gap_.lock(); gap_ = gap = upper_bound - lower_bound; - mutex_gap.unlock(); - - report(last_log, nodes_explored, heap.size(), gap, upper_bound, lower_bound, node_ptr->depth); + mutex_gap_.unlock(); + + f_t now = toc(stats_.start_time); + f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + if ((nodes_explored % 1000 == 0 || gap < 10 * settings_.absolute_mip_gap_tol || + nodes_explored < 1000) && + (time_since_log >= 1) || + (time_since_log > 60) || now > settings_.time_limit) { + settings_.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + heap.size(), + compute_user_objective(original_lp_, upper_bound), + compute_user_objective(original_lp_, lower_bound), + node_ptr->depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp_, upper_bound), + compute_user_objective(original_lp_, lower_bound)) + .c_str(), + now); + last_log = tic(); + } - if (toc(stats_.start_time) > settings.time_limit) { - settings.log.printf("Hit time limit. Stoppping\n"); + if (toc(stats_.start_time) > settings_.time_limit) { + settings_.log.printf("Hit time limit. Stoppping\n"); status = mip_status_t::TIME_LIMIT; break; } - status = - solve_leaf_lp(node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, heap.size()); + status = solve_leaf_lp( + node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, heap.size(), pc); if (status == mip_status_t::NUMERICAL) { break; } @@ -764,29 +761,26 @@ mip_status_t branch_and_bound_t::explore_tree(mip_node_t* st } } - stats_.unexplored_nodes = heap.size(); + stats_.nodes_unexplored = heap.size(); stats_.nodes_explored = nodes_explored; - if (stats_.unexplored_nodes == 0) { - mutex_lower.lock(); - lower_bound = lower_bound_ = root_node_->lower_bound; - mutex_lower.unlock(); + if (stats_.nodes_unexplored == 0) { + mutex_lower_.lock(); + lower_bound = lower_bound_ = root_node.lower_bound; + mutex_lower_.unlock(); - mutex_gap.lock(); + mutex_gap_.lock(); gap_ = gap = get_upper_bound() - lower_bound; - mutex_gap.unlock(); + mutex_gap_.unlock(); } return status; } template -mip_status_t branch_and_bound_t::dive(f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, +mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t& solution, - bool enable_reporting) + pseudo_costs_t pc) { mip_status_t status = mip_status_t::UNSET; @@ -795,58 +789,82 @@ mip_status_t branch_and_bound_t::dive(f_t root_objective, std::vector*> node_stack; - mip_node_t root_node(root_objective, root_vstatus); - graphviz_node(settings, &root_node, "lower bound", root_objective); + mip_node_t root_node(root_objective_, root_vstatus_); + graphviz_node(settings_, &root_node, "lower bound", root_objective_); - branch(&root_node, branch_var, branch_var_val, root_vstatus); + branch(&root_node, branch_var, root_relax_soln_.x[branch_var], root_vstatus_); // the stack does not own the unique_ptr the tree does node_stack.push_back(root_node.get_down_child()); node_stack.push_back(root_node.get_up_child()); // Make a copy of the original LP. We will modify its bounds at each leaf - lp_problem_t leaf_problem = original_lp; + lp_problem_t leaf_problem = original_lp_; f_t lower_bound = get_lower_bound(); f_t gap = get_upper_bound() - lower_bound; f_t last_log = 0; i_t nodes_explored = 0; - while (gap > settings.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), lower_bound) > settings.relative_mip_gap_tol && + while (gap > settings_.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) > settings_.relative_mip_gap_tol && node_stack.size() > 0) { repair_heuristic_solutions(lower_bound, solution); // Get a node off the stack mip_node_t* node_ptr = node_stack.back(); node_stack.pop_back(); + nodes_explored++; f_t upper_bound = get_upper_bound(); lower_bound = get_lower_bound(); gap = upper_bound - lower_bound; const i_t leaf_depth = node_ptr->depth; - if (enable_reporting) { - report( - last_log, nodes_explored, node_stack.size(), gap, upper_bound, lower_bound, leaf_depth); + f_t now = toc(stats_.start_time); + f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + + if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + if ((nodes_explored % 1000 == 0 || gap < 10 * settings_.absolute_mip_gap_tol || + nodes_explored < 1000) && + (time_since_log >= 1) || + (time_since_log > 60) || now > settings_.time_limit) { + settings_.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + nodes_explored, + node_stack.size(), + compute_user_objective(original_lp_, upper_bound), + compute_user_objective(original_lp_, lower_bound), + leaf_depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(compute_user_objective(original_lp_, upper_bound), + compute_user_objective(original_lp_, lower_bound)) + .c_str(), + now); + last_log = tic(); + } } - if (toc(stats_.start_time) > settings.time_limit) { - if (enable_reporting) { settings.log.printf("Hit time limit. Stoppping\n"); } + if (toc(stats_.start_time) > settings_.time_limit) { + if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + settings_.log.printf("Hit time limit. Stoppping\n"); + } + status = mip_status_t::TIME_LIMIT; break; } status = solve_leaf_lp( - node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, node_stack.size()); + node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, node_stack.size(), pc); if (status == mip_status_t::NUMERICAL) { break; } if (node_ptr->status == node_status_t::HAS_CHILDREN) { // Martin's child selection - const f_t down_val = std::floor(root_node_->fractional_val); - const f_t up_val = std::ceil(root_node_->fractional_val); - const f_t down_dist = node_ptr->get_down_child()->fractional_val - down_val; - const f_t up_dist = up_val - node_ptr->get_down_child()->fractional_val; + const i_t branch_var = node_ptr->get_down_child()->branch_var; + const f_t branch_var_val = node_ptr->get_down_child()->fractional_val; + const f_t down_val = std::floor(root_relax_soln_.x[branch_var]); + const f_t up_val = std::ceil(root_relax_soln_.x[branch_var]); + const f_t down_dist = branch_var_val - down_val; + const f_t up_dist = up_val - branch_var_val; if (down_dist < up_dist) { node_stack.push_back(node_ptr->get_up_child()); @@ -858,18 +876,20 @@ mip_status_t branch_and_bound_t::dive(f_t root_objective, } } - stats_.unexplored_nodes = node_stack.size(); - stats_.nodes_explored = nodes_explored; + if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + stats_.nodes_unexplored = node_stack.size(); + stats_.nodes_explored = nodes_explored; - if (stats_.unexplored_nodes == 0) { - mutex_lower.lock(); - lower_bound = lower_bound_ = root_node.lower_bound; - mutex_lower.unlock(); - gap = get_upper_bound() - lower_bound; + if (stats_.nodes_unexplored == 0) { + mutex_lower_.lock(); + lower_bound = lower_bound_ = root_node.lower_bound; + mutex_lower_.unlock(); + gap = get_upper_bound() - lower_bound; - mutex_gap.lock(); - gap_ = gap; - mutex_gap.unlock(); + mutex_gap_.lock(); + gap_ = gap; + mutex_gap_.unlock(); + } } return status; @@ -880,120 +900,108 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut { mip_status_t status = mip_status_t::UNSET; - if (guess.size() != 0) { + if (guess_.size() != 0) { std::vector crushed_guess; - crush_primal_solution(original_problem, original_lp, guess, new_slacks, crushed_guess); + crush_primal_solution(original_problem_, original_lp_, guess_, new_slacks_, crushed_guess); f_t primal_err; f_t bound_err; i_t num_fractional; const bool feasible = check_guess( - original_lp, settings, var_types, crushed_guess, primal_err, bound_err, num_fractional); + original_lp_, settings_, var_types_, crushed_guess, primal_err, bound_err, num_fractional); if (feasible) { - const f_t computed_obj = compute_objective(original_lp, crushed_guess); - mutex_upper.lock(); + const f_t computed_obj = compute_objective(original_lp_, crushed_guess); + mutex_upper_.lock(); incumbent_.set_incumbent_solution(computed_obj, crushed_guess); upper_bound_ = computed_obj; - mutex_upper.unlock(); + mutex_upper_.unlock(); } } - lp_solution_t root_relax_soln(original_lp.num_rows, original_lp.num_cols); - f_t root_objective; - - mip_status_t root_status = solve_root_relaxation(root_objective, root_relax_soln, root_vstatus); + root_relax_soln_.resize(original_lp_.num_rows, original_lp_.num_cols); + mip_status_t root_status = solve_root_relaxation(); if (root_status != mip_status_t::UNSET) { return root_status; } std::vector fractional; const i_t num_fractional = - fractional_variables(settings, root_relax_soln.x, var_types, fractional); + fractional_variables(settings_, root_relax_soln_.x, var_types_, fractional); if (num_fractional == 0) { - mutex_upper.lock(); - incumbent_.set_incumbent_solution(root_objective, root_relax_soln.x); - upper_bound_ = root_objective; - mutex_upper.unlock(); + mutex_upper_.lock(); + incumbent_.set_incumbent_solution(root_objective_, root_relax_soln_.x); + upper_bound_ = root_objective_; + mutex_upper_.unlock(); // We should be done here - uncrush_primal_solution(original_problem, original_lp, incumbent_.x, solution.x); + uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); solution.objective = incumbent_.objective; solution.lower_bound = lower_bound_; solution.nodes_explored = 0; - solution.simplex_iterations = root_relax_soln.iterations; - settings.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", - compute_user_objective(original_lp, root_objective), - toc(stats_.start_time)); - if (settings.set_simplex_solution_callback != nullptr) { - settings.set_simplex_solution_callback(solution.x, solution.objective); + solution.simplex_iterations = root_relax_soln_.iterations; + settings_.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", + compute_user_objective(original_lp_, root_objective_), + toc(stats_.start_time)); + if (settings_.set_simplex_solution_callback != nullptr) { + settings_.set_simplex_solution_callback(solution.x, solution.objective); } - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); } return mip_status_t::OPTIMAL; } - pc.initialize(original_lp.num_cols); - strong_branching(original_lp, - settings, + pseudo_costs_t pc(original_lp_.num_cols); + strong_branching(original_lp_, + settings_, stats_.start_time, - var_types, - root_relax_soln.x, + var_types_, + root_relax_soln_.x, fractional, - root_objective, - root_vstatus, - edge_norms, + root_objective_, + root_vstatus_, + edge_norms_, pc); // Choose variable to branch on logger_t log; - log.log = false; - i_t branch_var = - pc.variable_selection(fractional, root_relax_soln.x, original_lp.lower, original_lp.upper, log); + log.log = false; + i_t branch_var = pc.variable_selection( + fractional, root_relax_soln_.x, original_lp_.lower, original_lp_.upper, log); stats_.total_lp_iters = 0; stats_.nodes_explored = 0; - stats_.unexplored_nodes = 0; + stats_.nodes_unexplored = 0; stats_.num_nodes = 1; - root_node_ = std::make_unique>(root_objective, root_vstatus); - graphviz_node(settings, root_node_.get(), "lower bound", root_objective); - root_node_->fractional_val = root_relax_soln.x[branch_var]; - root_node_->branch_var = branch_var; - - branch(root_node_.get(), branch_var, root_relax_soln.x[branch_var], root_vstatus); - - if (settings.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { - settings.log.printf("Using depth first search\n"); - } else if (settings.bnb_search_strategy == + if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + settings_.log.printf("Using depth first search\n"); + } else if (settings_.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - settings.log.printf("Using best first search with diving\n"); + settings_.log.printf("Using best first search with diving\n"); } else { - settings.log.printf("Using best first search\n"); + settings_.log.printf("Using best first search\n"); } - settings.log.printf( + settings_.log.printf( "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " " Time \n"); - mutex_branching.lock(); + mutex_branching_.lock(); currently_branching_ = true; - mutex_branching.unlock(); + mutex_branching_.unlock(); - if (settings.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { - status = - dive(root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, true); + if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + status = dive(branch_var, solution, std::move(pc)); } else { std::future diving_thread; - if (settings.bnb_search_strategy == + if (settings_.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - diving_thread = std::async(std::launch::async, [&]() { - return dive( - root_objective, branch_var, root_node_->fractional_val, root_vstatus, solution, false); - }); + diving_thread = + std::async(std::launch::async, [&]() { return dive(branch_var, solution, pc); }); } - status = explore_tree(root_node_.get(), root_objective, solution); + status = explore_tree(branch_var, solution, pc); - if (settings.bnb_search_strategy == + if (settings_.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { // TODO: Instead of waiting, we should send a signal that we are already done in the main // thread @@ -1001,45 +1009,45 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - mutex_branching.lock(); + mutex_branching_.lock(); currently_branching_ = false; - mutex_branching.unlock(); + mutex_branching_.unlock(); - settings.log.printf( + settings_.log.printf( "Explored %d nodes in %.2fs.\nAbsolute Gap %e Objective %.16e Lower Bound %.16e\n", stats_.nodes_explored.load(), toc(stats_.start_time), gap_, - compute_user_objective(original_lp, get_upper_bound()), - compute_user_objective(original_lp, lower_bound_)); + compute_user_objective(original_lp_, get_upper_bound()), + compute_user_objective(original_lp_, lower_bound_)); - if (gap_ <= settings.absolute_mip_gap_tol || - relative_gap(get_upper_bound(), lower_bound_) <= settings.relative_mip_gap_tol) { + if (gap_ <= settings_.absolute_mip_gap_tol || + relative_gap(get_upper_bound(), lower_bound_) <= settings_.relative_mip_gap_tol) { status = mip_status_t::OPTIMAL; - if (gap_ > 0 && gap_ <= settings.absolute_mip_gap_tol) { - settings.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", - settings.absolute_mip_gap_tol); + if (gap_ > 0 && gap_ <= settings_.absolute_mip_gap_tol) { + settings_.log.printf("Optimal solution found within absolute MIP gap tolerance (%.1e)\n", + settings_.absolute_mip_gap_tol); } else if (gap_ > 0 && - relative_gap(get_upper_bound(), lower_bound_) <= settings.relative_mip_gap_tol) { - settings.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", - settings.relative_mip_gap_tol); + relative_gap(get_upper_bound(), lower_bound_) <= settings_.relative_mip_gap_tol) { + settings_.log.printf("Optimal solution found within relative MIP gap tolerance (%.1e)\n", + settings_.relative_mip_gap_tol); } else { - settings.log.printf("Optimal solution found.\n"); + settings_.log.printf("Optimal solution found.\n"); } - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); } } - if (stats_.unexplored_nodes == 0 && get_upper_bound() == inf) { - settings.log.printf("Integer infeasible.\n"); + if (stats_.nodes_unexplored == 0 && get_upper_bound() == inf) { + settings_.log.printf("Integer infeasible.\n"); status = mip_status_t::INFEASIBLE; - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); } } - uncrush_primal_solution(original_problem, original_lp, incumbent_.x, solution.x); + uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, solution.x); solution.objective = incumbent_.objective; solution.lower_bound = get_lower_bound(); solution.nodes_explored = stats_.nodes_explored; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 7e3875eaa8..403c2b9807 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -56,13 +56,12 @@ class branch_and_bound_t { const simplex_solver_settings_t& solver_settings); // Set an initial guess based on the user_problem. This should be called before solve. - void set_initial_guess(const std::vector& user_guess) { guess = user_guess; } + void set_initial_guess(const std::vector& user_guess) { guess_ = user_guess; } // Set a solution based on the user problem during the course of the solve void set_new_solution(const std::vector& solution); - bool repair_solution(const std::vector& root_vstatus, - const std::vector& leaf_edge_norms, + bool repair_solution(const std::vector& leaf_edge_norms, const std::vector& potential_solution, f_t& repaired_obj, std::vector& repaired_solution) const; @@ -75,97 +74,82 @@ class branch_and_bound_t { mip_status_t solve(mip_solution_t& solution); private: - const user_problem_t& original_problem; - const simplex_solver_settings_t settings; + const user_problem_t& original_problem_; + const simplex_solver_settings_t settings_; - std::vector guess; + std::vector guess_; - lp_problem_t original_lp; - std::vector new_slacks; - std::vector var_types; + lp_problem_t original_lp_; + std::vector new_slacks_; + std::vector var_types_; // Mutex for lower bound - std::mutex mutex_lower; + std::mutex mutex_lower_; // Global variable for lower bound f_t lower_bound_; // Mutex for upper bound - std::mutex mutex_upper; + std::mutex mutex_upper_; // Global variable for upper bound f_t upper_bound_; // Global variable for incumbent. The incumbent should be updated with the upper bound mip_solution_t incumbent_; // Mutex for gap - std::mutex mutex_gap; + std::mutex mutex_gap_; // Global variable for gap f_t gap_; // Mutex for branching - std::mutex mutex_branching; + std::mutex mutex_branching_; bool currently_branching_; // Global variable for stats - std::mutex mutex_stats; + std::mutex mutex_stats_; // Note that floating point atomics are only supported in C++20 struct stats_t { f_t start_time = 0.0; f_t total_lp_solve_time = 0.0; std::atomic nodes_explored = 0; - std::atomic unexplored_nodes = 0; + std::atomic nodes_unexplored = 0; f_t total_lp_iters = 0; std::atomic num_nodes = 0; } stats_; // Mutex for repair - std::mutex mutex_repair; - std::vector> repair_queue; - - // Pseudocosts - std::mutex mutex_pseudocosts; - pseudo_costs_t pc; + std::mutex mutex_repair_; + std::vector> repair_queue_; // Search tree - std::unique_ptr> root_node_; - std::vector root_vstatus; - std::vector edge_norms; + std::vector root_vstatus_; + f_t root_objective_; + lp_solution_t root_relax_soln_; + std::vector edge_norms_; void repair_heuristic_solutions(const f_t& lower_bound, mip_solution_t& solution); - void report(f_t last_log, - i_t nodes_explored, - i_t nodes_unexplored, - f_t gap, - f_t upper_bound, - f_t lower_bound, - i_t leaf_depth); - - mip_status_t explore_tree(mip_node_t* start_node, - f_t root_objective, - mip_solution_t& solution); - - mip_status_t dive(f_t root_objective, - i_t branch_var, - f_t branch_var_val, - std::vector& root_vstatus, + mip_status_t explore_tree(i_t branch_var, + mip_solution_t& solution, + pseudo_costs_t pc); + + mip_status_t dive(i_t branch_var, mip_solution_t& solution, - bool enable_reporting); + pseudo_costs_t pc); void branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, const std::vector& parent_vstatus); - mip_status_t solve_root_relaxation(f_t& root_objective, - lp_solution_t& root_relax_soln, - std::vector& root_vstatus); + mip_status_t solve_root_relaxation(); mip_status_t solve_leaf_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, f_t upper_bound, f_t lower_bound, i_t nodes_explored, - i_t unexplored_nodes); + i_t unexplored_nodes, + pseudo_costs_t& pc); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 5065b8525d..724ac83f13 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -29,11 +29,6 @@ namespace cuopt::linear_programming::dual_simplex { template class pseudo_costs_t { public: - pseudo_costs_t() - : pseudo_cost_sum_down(0), pseudo_cost_sum_up(0), pseudo_cost_num_down(0), pseudo_cost_num_up(0) - { - } - explicit pseudo_costs_t(i_t num_variables) : pseudo_cost_sum_down(num_variables), pseudo_cost_sum_up(num_variables), @@ -42,12 +37,26 @@ class pseudo_costs_t { { } - void initialize(i_t num_variables) + pseudo_costs_t(const pseudo_costs_t& other) + : pseudo_cost_sum_down(other.pseudo_cost_sum_down), + pseudo_cost_sum_up(other.pseudo_cost_sum_up), + pseudo_cost_num_down(other.pseudo_cost_num_down), + pseudo_cost_num_up(other.pseudo_cost_num_up), + strong_branch_down(other.strong_branch_down), + strong_branch_up(other.strong_branch_up), + num_strong_branches_completed(other.num_strong_branches_completed) + { + } + + pseudo_costs_t(pseudo_costs_t&& other) + : pseudo_cost_sum_down(std::move(other.pseudo_cost_sum_down)), + pseudo_cost_sum_up(std::move(other.pseudo_cost_sum_up)), + pseudo_cost_num_down(std::move(other.pseudo_cost_num_down)), + pseudo_cost_num_up(std::move(other.pseudo_cost_num_up)), + strong_branch_down(std::move(other.strong_branch_down)), + strong_branch_up(std::move(other.strong_branch_up)), + num_strong_branches_completed(other.num_strong_branches_completed) { - pseudo_cost_sum_down.resize(num_variables); - pseudo_cost_sum_up.resize(num_variables); - pseudo_cost_num_down.resize(num_variables); - pseudo_cost_num_up.resize(num_variables); } void update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective); From af177e963b632e097b4578e839cc1512f1e5524e Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 28 Aug 2025 11:49:33 +0200 Subject: [PATCH 25/44] Added tests --- .../linear_programming/cuopt/run_mip.cpp | 17 ++++++------ cpp/src/dual_simplex/branch_and_bound.hpp | 17 ++++++++++-- cpp/tests/mip/miplib_test.cu | 26 ++++++++++++++++--- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 85f32a27ce..75b7862096 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -236,15 +236,14 @@ int run_single_file(std::string file_path, } else { CUOPT_LOG_INFO("%s: no solution found", base_filename.c_str()); } - // std::stringstream ss; - // int decimal_places = 2; - // ss << std::fixed << std::setprecision(decimal_places) << base_filename << "," << sol_found << - // "," - // << obj_val << "," << benchmark_info.objective_of_initial_population << "," - // << benchmark_info.last_improvement_of_best_feasible << "," - // << benchmark_info.last_improvement_after_recombination << "\n"; - // write_to_output_file(out_dir, base_filename, device, batch_id, ss.str()); - // CUOPT_LOG_INFO("Results written to the file %s", base_filename.c_str()); + std::stringstream ss; + int decimal_places = 2; + ss << std::fixed << std::setprecision(decimal_places) << base_filename << "," << sol_found << "," + << obj_val << "," << benchmark_info.objective_of_initial_population << "," + << benchmark_info.last_improvement_of_best_feasible << "," + << benchmark_info.last_improvement_after_recombination << "\n"; + write_to_output_file(out_dir, base_filename, device, batch_id, ss.str()); + CUOPT_LOG_INFO("Results written to the file %s", base_filename.c_str()); return sol_found; } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 403c2b9807..a29e9b54ff 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -61,6 +61,7 @@ class branch_and_bound_t { // Set a solution based on the user problem during the course of the solve void set_new_solution(const std::vector& solution); + // Repair a low-quality solution from the heuristics. bool repair_solution(const std::vector& leaf_edge_norms, const std::vector& potential_solution, f_t& repaired_obj, @@ -77,25 +78,31 @@ class branch_and_bound_t { const user_problem_t& original_problem_; const simplex_solver_settings_t settings_; + // Initial guess. std::vector guess_; lp_problem_t original_lp_; std::vector new_slacks_; std::vector var_types_; + // Mutex for lower bound std::mutex mutex_lower_; + // Global variable for lower bound f_t lower_bound_; // Mutex for upper bound std::mutex mutex_upper_; + // Global variable for upper bound f_t upper_bound_; + // Global variable for incumbent. The incumbent should be updated with the upper bound mip_solution_t incumbent_; // Mutex for gap std::mutex mutex_gap_; + // Global variable for gap f_t gap_; @@ -106,7 +113,7 @@ class branch_and_bound_t { // Global variable for stats std::mutex mutex_stats_; - // Note that floating point atomics are only supported in C++20 + // Note that floating point atomics are only supported in C++20. struct stats_t { f_t start_time = 0.0; f_t total_lp_solve_time = 0.0; @@ -120,29 +127,35 @@ class branch_and_bound_t { std::mutex mutex_repair_; std::vector> repair_queue_; - // Search tree + // Variables for the root node in the search tree. std::vector root_vstatus_; f_t root_objective_; lp_solution_t root_relax_soln_; std::vector edge_norms_; + // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(const f_t& lower_bound, mip_solution_t& solution); + // Explore the search tree using the best-first search strategy. mip_status_t explore_tree(i_t branch_var, mip_solution_t& solution, pseudo_costs_t pc); + // Explore the search tree using the depth-first search strategy. mip_status_t dive(i_t branch_var, mip_solution_t& solution, pseudo_costs_t pc); + // Branch the current node, creating two children. void branch(mip_node_t* parent_node, i_t branch_var, f_t branch_var_val, const std::vector& parent_vstatus); + // Solve the LP relaxation of the root node. mip_status_t solve_root_relaxation(); + // Solve the LP relaxation of a leaf node. mip_status_t solve_leaf_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, f_t upper_bound, diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index 603fe60145..28f593f821 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -16,6 +16,7 @@ */ #include "../linear_programming/utilities/pdlp_test_utilities.cuh" +#include "cuopt/linear_programming/mip/solver_settings.hpp" #include "mip_utils.cuh" #include @@ -40,7 +41,7 @@ struct result_map_t { double cost; }; -void test_miplib_file(result_map_t test_instance) +void test_miplib_file(result_map_t test_instance, mip_solver_settings_t settings) { const raft::handle_t handle_{}; @@ -48,7 +49,6 @@ void test_miplib_file(result_map_t test_instance) cuopt::mps_parser::mps_data_model_t problem = cuopt::mps_parser::parse_mps(path, false); handle_.sync_stream(); - mip_solver_settings_t settings; // set the time limit depending on we are in assert mode or not #ifdef ASSERT_MODE constexpr double test_time_limit = 60.; @@ -68,10 +68,30 @@ void test_miplib_file(result_map_t test_instance) TEST(mip_solve, run_small_tests) { + mip_solver_settings_t settings; std::vector test_instances = { {"mip/50v-10.mps", 11311031.}, {"mip/neos5.mps", 15.}, {"mip/swath1.mps", 1300.}}; for (const auto& test_instance : test_instances) { - test_miplib_file(test_instance); + test_miplib_file(test_instance, settings); + } +} + +TEST(mip_solve, bnb_search_strategy) +{ + mip_solver_settings_t settings; + std::vector test_instances = { + {"mip/50v-10.mps", 11311031.}, {"mip/neos5.mps", 15.}, {"mip/swath1.mps", 1300.}}; + + std::vector strategies = { + bnb_search_strategy_t::BEST_FIRST, + bnb_search_strategy_t::DEPTH_FIRST, + bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING}; + + for (const auto& search : strategies) { + for (const auto& test_instance : test_instances) { + settings.bnb_search_strategy = search; + test_miplib_file(test_instance, settings); + } } } From 30c033343ae09ccada1577537d84044f6a2e75ae Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 29 Aug 2025 15:32:45 +0200 Subject: [PATCH 26/44] changed code based on reviewer's feedback --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- cpp/src/dual_simplex/branch_and_bound.cpp | 6 +++--- cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- cpp/src/dual_simplex/mip_node.hpp | 11 ++++------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 75b7862096..808ce91326 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -176,7 +176,7 @@ int run_single_file(std::string file_path, cuopt::mps_parser::mps_data_model_t mps_data_model; bool parsing_failed = false; { - CUOPT_LOG_INFO("Running file %s", base_filename.c_str()); + CUOPT_LOG_INFO("running file %s on gpu : %d", base_filename.c_str(), device); try { mps_data_model = cuopt::mps_parser::parse_mps(file_path, input_mps_strict); } catch (const std::logic_error& e) { diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b37d032d78..f75ea0f1d1 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -518,7 +518,7 @@ mip_status_t branch_and_bound_t::solve_root_relaxation() } template -mip_status_t branch_and_bound_t::solve_leaf_lp(mip_node_t* node_ptr, +mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, f_t upper_bound, f_t lower_bound, @@ -749,7 +749,7 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, break; } - status = solve_leaf_lp( + status = solve_node_lp( node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, heap.size(), pc); if (status == mip_status_t::NUMERICAL) { break; } @@ -853,7 +853,7 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, break; } - status = solve_leaf_lp( + status = solve_node_lp( node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, node_stack.size(), pc); if (status == mip_status_t::NUMERICAL) { break; } diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index a29e9b54ff..0f102dbb60 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -156,7 +156,7 @@ class branch_and_bound_t { mip_status_t solve_root_relaxation(); // Solve the LP relaxation of a leaf node. - mip_status_t solve_leaf_lp(mip_node_t* node_ptr, + mip_status_t solve_node_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, f_t upper_bound, f_t lower_bound, diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 5413798ae4..21d8351b6f 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -141,15 +141,12 @@ class mip_node_t { } } - node_status_t set_status(node_status_t new_status) - { - return status.exchange(new_status, std::memory_order_seq_cst); - } + node_status_t set_status(node_status_t new_status) { return status; } // outputs a stack containing inactive nodes in the tree that can be freed void set_status(node_status_t node_status, std::vector& stack) { - status.store(node_status, std::memory_order_seq_cst); + status = node_status; if (inactive_status(status)) { update_bound(); stack.push_back(this); @@ -157,7 +154,7 @@ class mip_node_t { mip_node_t* parent_ptr = parent; while (parent_ptr != nullptr) { if (parent_ptr->is_inactive()) { - parent_ptr->status.store(node_status_t::FATHOMED, std::memory_order_seq_cst); + parent_ptr->status = node_status_t::FATHOMED; parent_ptr->update_bound(); stack.push_back(parent_ptr); } else { @@ -201,7 +198,7 @@ class mip_node_t { } } - std::atomic status; + node_status_t status; f_t lower_bound; i_t depth; i_t node_id; From ae33427ed0f71447092bebc51c9fd8f7f8dc2b93 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 29 Aug 2025 16:11:18 +0200 Subject: [PATCH 27/44] more changes based on feedback --- benchmarks/linear_programming/cuopt/run_mip.cpp | 10 ---------- cpp/include/cuopt/linear_programming/constants.h | 6 ++++++ cpp/src/dual_simplex/branch_and_bound.cpp | 8 ++++---- cpp/src/dual_simplex/branch_and_bound.hpp | 2 +- cpp/src/math_optimization/solver_settings.cu | 3 ++- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 808ce91326..9c254d7b25 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -25,7 +25,6 @@ #include #include -#include #include #include @@ -350,11 +349,6 @@ int main(int argc, char* argv[]) .help("Search strategy used in B&B (bfs/bfs-diving/dfs)") .default_value(std::string("bfs")); - program.add_argument("--gpu") - .help("id of the GPU to use when running a single test (default: 0)") - .scan<'i', int>() - .default_value(0); - // Parse arguments try { program.parse_args(argc, argv); @@ -382,7 +376,6 @@ int main(int argc, char* argv[]) bool write_log_file = program.get("--write-log-file")[0] == 't'; bool log_to_console = program.get("--log-to-console")[0] == 't'; std::string search_strategy_cli = program.get("--search-strategy"); - int gpu_id = program.get("--gpu"); cuopt::linear_programming::bnb_search_strategy_t search_strategy; if (search_strategy_cli == "bfs") { @@ -499,9 +492,6 @@ int main(int argc, char* argv[]) } merge_result_files(out_dir, result_file, n_gpus, batch_num); } else { - RAFT_CUDA_TRY(cudaSetDevice(gpu_id)); - CUOPT_LOG_INFO("Using GPU %d", gpu_id); - auto memory_resource = make_async(); rmm::mr::set_current_device_resource(memory_resource.get()); run_single_file(path, diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 72d05a6adb..9797575417 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -62,6 +62,7 @@ #define CUOPT_SOLUTION_FILE "solution_file" #define CUOPT_NUM_CPU_THREADS "num_cpu_threads" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" +#define CUOPT_BNB_SEARCH_STRATEGY "bnb_search_strategy" /* @brief LP/MIP termination status constants */ #define CUOPT_TERIMINATION_STATUS_NO_TERMINATION 0 @@ -115,4 +116,9 @@ #define CUOPT_OUT_OF_MEMORY 5 #define CUOPT_RUNTIME_ERROR 6 +/* @brief BNB search strategy constants */ +#define CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST 0 +#define CUOPT_BNB_SEARCH_STRATEGY_DEPTH_FIRST 1 +#define CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING 2 + #endif // CUOPT_CONSTANTS_H diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index f75ea0f1d1..c4a1e8beaa 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -221,7 +221,7 @@ f_t relative_gap(f_t obj_value, f_t lower_bound) ? (lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) : std::abs(obj_value - lower_bound) / std::abs(obj_value); // Handle NaNs (i.e., NaN != NaN) - if (user_mip_gap != user_mip_gap) { return std::numeric_limits::infinity(); } + if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } return user_mip_gap; } @@ -397,7 +397,7 @@ branch_and_bound_t::branch_and_bound_t( } template -void branch_and_bound_t::repair_heuristic_solutions(const f_t& lower_bound, +void branch_and_bound_t::repair_heuristic_solutions(f_t lower_bound, mip_solution_t& solution) { // Check if there are any solutions to repair @@ -454,14 +454,14 @@ void branch_and_bound_t::branch(mip_node_t* parent_node, const std::vector& parent_vstatus) { // down child - std::unique_ptr> down_child = std::make_unique>( + auto down_child = std::make_unique>( original_lp_, parent_node, ++stats_.num_nodes, branch_var, 0, branch_var_val, parent_vstatus); graphviz_edge( settings_, parent_node, down_child.get(), branch_var, 0, std::floor(branch_var_val)); // up child - std::unique_ptr> up_child = std::make_unique>( + auto up_child = std::make_unique>( original_lp_, parent_node, ++stats_.num_nodes, branch_var, 1, branch_var_val, parent_vstatus); graphviz_edge(settings_, parent_node, up_child.get(), branch_var, 1, std::ceil(branch_var_val)); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0f102dbb60..0c71bf52a7 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -134,7 +134,7 @@ class branch_and_bound_t { std::vector edge_norms_; // Repairs low-quality solutions from the heuristics, if it is applicable. - void repair_heuristic_solutions(const f_t& lower_bound, mip_solution_t& solution); + void repair_heuristic_solutions(f_t lower_bound, mip_solution_t& solution); // Explore the search tree using the best-first search strategy. mip_status_t explore_tree(i_t branch_var, diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index a049d0d09b..97310c77bd 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -90,7 +90,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_ITERATION_LIMIT, &pdlp_settings.iteration_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, {CUOPT_PDLP_SOLVER_MODE, reinterpret_cast(&pdlp_settings.pdlp_solver_mode), CUOPT_PDLP_SOLVER_MODE_STABLE1, CUOPT_PDLP_SOLVER_MODE_FAST1, CUOPT_PDLP_SOLVER_MODE_STABLE2}, {CUOPT_METHOD, reinterpret_cast(&pdlp_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_DUAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, - {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1} + {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1}, + {CUOPT_BNB_SEARCH_STRATEGY, reinterpret_cast(&mip_settings.bnb_search_strategy), CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST, CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING, CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST} }; // Bool parameters From 0017b5038e04c54fe4cf9bf2a798c4f8f6931c07 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 29 Aug 2025 16:24:18 +0200 Subject: [PATCH 28/44] moved pseudocost to be a global variable --- cpp/src/dual_simplex/branch_and_bound.cpp | 40 +++++++++++------------ cpp/src/dual_simplex/branch_and_bound.hpp | 15 ++++----- cpp/src/dual_simplex/pseudo_costs.hpp | 26 ++++----------- 3 files changed, 33 insertions(+), 48 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index c4a1e8beaa..f5fbae682d 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -377,7 +377,8 @@ branch_and_bound_t::branch_and_bound_t( settings_(solver_settings), original_lp_(1, 1, 1), incumbent_(1), - root_relax_soln_(1, 1) + root_relax_soln_(1, 1), + pc_(1) { stats_.start_time = tic(); convert_user_problem(original_problem_, settings_, original_lp_, new_slacks_); @@ -523,8 +524,7 @@ mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* n f_t upper_bound, f_t lower_bound, i_t nodes_explored, - i_t unexplored_nodes, - pseudo_costs_t& pc) + i_t unexplored_nodes) { logger_t log; log.log = false; @@ -590,7 +590,9 @@ mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* n f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); graphviz_node(settings_, node_ptr, "lower bound", leaf_objective); - pc.update_pseudo_costs(node_ptr, leaf_objective); + mutex_pc_.lock(); + pc_.update_pseudo_costs(node_ptr, leaf_objective); + mutex_pc_.unlock(); node_ptr->lower_bound = leaf_objective; @@ -638,8 +640,10 @@ mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* n } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on - const i_t branch_var = pc.variable_selection( + mutex_pc_.lock(); + const i_t branch_var = pc_.variable_selection( fractional, leaf_solution.x, leaf_problem.lower, leaf_problem.upper, log); + mutex_pc_.unlock(); assert(leaf_vstatus.size() == leaf_problem.num_cols); branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); @@ -663,8 +667,7 @@ mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* n template mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, - mip_solution_t& solution, - pseudo_costs_t pc) + mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; logger_t log; @@ -749,8 +752,8 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, break; } - status = solve_node_lp( - node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, heap.size(), pc); + status = + solve_node_lp(node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, heap.size()); if (status == mip_status_t::NUMERICAL) { break; } @@ -778,9 +781,7 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, } template -mip_status_t branch_and_bound_t::dive(i_t branch_var, - mip_solution_t& solution, - pseudo_costs_t pc) +mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t& solution) { mip_status_t status = mip_status_t::UNSET; @@ -854,7 +855,7 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, } status = solve_node_lp( - node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, node_stack.size(), pc); + node_ptr, leaf_problem, upper_bound, lower_bound, nodes_explored, node_stack.size()); if (status == mip_status_t::NUMERICAL) { break; } if (node_ptr->status == node_status_t::HAS_CHILDREN) { @@ -948,7 +949,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::OPTIMAL; } - pseudo_costs_t pc(original_lp_.num_cols); + pc_.initialize(original_lp_.num_cols); strong_branching(original_lp_, settings_, stats_.start_time, @@ -958,12 +959,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_objective_, root_vstatus_, edge_norms_, - pc); + pc_); // Choose variable to branch on logger_t log; log.log = false; - i_t branch_var = pc.variable_selection( + i_t branch_var = pc_.variable_selection( fractional, root_relax_soln_.x, original_lp_.lower, original_lp_.upper, log); stats_.total_lp_iters = 0; @@ -989,17 +990,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut mutex_branching_.unlock(); if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { - status = dive(branch_var, solution, std::move(pc)); + status = dive(branch_var, solution); } else { std::future diving_thread; if (settings_.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - diving_thread = - std::async(std::launch::async, [&]() { return dive(branch_var, solution, pc); }); + diving_thread = std::async(std::launch::async, [&]() { return dive(branch_var, solution); }); } - status = explore_tree(branch_var, solution, pc); + status = explore_tree(branch_var, solution); if (settings_.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 0c71bf52a7..da9914a5ef 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -133,18 +133,18 @@ class branch_and_bound_t { lp_solution_t root_relax_soln_; std::vector edge_norms_; + // Pseudocosts + pseudo_costs_t pc_; + std::mutex mutex_pc_; + // Repairs low-quality solutions from the heuristics, if it is applicable. void repair_heuristic_solutions(f_t lower_bound, mip_solution_t& solution); // Explore the search tree using the best-first search strategy. - mip_status_t explore_tree(i_t branch_var, - mip_solution_t& solution, - pseudo_costs_t pc); + mip_status_t explore_tree(i_t branch_var, mip_solution_t& solution); // Explore the search tree using the depth-first search strategy. - mip_status_t dive(i_t branch_var, - mip_solution_t& solution, - pseudo_costs_t pc); + mip_status_t dive(i_t branch_var, mip_solution_t& solution); // Branch the current node, creating two children. void branch(mip_node_t* parent_node, @@ -161,8 +161,7 @@ class branch_and_bound_t { f_t upper_bound, f_t lower_bound, i_t nodes_explored, - i_t unexplored_nodes, - pseudo_costs_t& pc); + i_t unexplored_nodes); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 724ac83f13..444e8c7ac5 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -37,30 +37,16 @@ class pseudo_costs_t { { } - pseudo_costs_t(const pseudo_costs_t& other) - : pseudo_cost_sum_down(other.pseudo_cost_sum_down), - pseudo_cost_sum_up(other.pseudo_cost_sum_up), - pseudo_cost_num_down(other.pseudo_cost_num_down), - pseudo_cost_num_up(other.pseudo_cost_num_up), - strong_branch_down(other.strong_branch_down), - strong_branch_up(other.strong_branch_up), - num_strong_branches_completed(other.num_strong_branches_completed) - { - } + void update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective); - pseudo_costs_t(pseudo_costs_t&& other) - : pseudo_cost_sum_down(std::move(other.pseudo_cost_sum_down)), - pseudo_cost_sum_up(std::move(other.pseudo_cost_sum_up)), - pseudo_cost_num_down(std::move(other.pseudo_cost_num_down)), - pseudo_cost_num_up(std::move(other.pseudo_cost_num_up)), - strong_branch_down(std::move(other.strong_branch_down)), - strong_branch_up(std::move(other.strong_branch_up)), - num_strong_branches_completed(other.num_strong_branches_completed) + void initialize(i_t num_variables) { + pseudo_cost_sum_down.resize(num_variables); + pseudo_cost_sum_up.resize(num_variables); + pseudo_cost_num_down.resize(num_variables); + pseudo_cost_num_up.resize(num_variables); } - void update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective); - void initialized(i_t& num_initialized_down, i_t& num_initialized_up, f_t& pseudo_cost_down_avg, From f2f029a9ffc2fd7debe042a847f12291faf7ed3b Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 29 Aug 2025 16:37:52 +0200 Subject: [PATCH 29/44] fix incorrect set status --- cpp/src/dual_simplex/mip_node.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 21d8351b6f..bf6f5c91ab 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -141,7 +141,7 @@ class mip_node_t { } } - node_status_t set_status(node_status_t new_status) { return status; } + void set_status(node_status_t new_status) { status = new_status; } // outputs a stack containing inactive nodes in the tree that can be freed void set_status(node_status_t node_status, std::vector& stack) From 2f51ac068cb5c647b9b389e6bdabe3b14e9249ca Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 29 Aug 2025 18:28:12 +0200 Subject: [PATCH 30/44] fixing failing tests --- cpp/src/math_optimization/solver_settings.cu | 2 +- cpp/tests/mip/miplib_test.cu | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 97310c77bd..b9c147801c 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -91,7 +91,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_PDLP_SOLVER_MODE, reinterpret_cast(&pdlp_settings.pdlp_solver_mode), CUOPT_PDLP_SOLVER_MODE_STABLE1, CUOPT_PDLP_SOLVER_MODE_FAST1, CUOPT_PDLP_SOLVER_MODE_STABLE2}, {CUOPT_METHOD, reinterpret_cast(&pdlp_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_DUAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1}, - {CUOPT_BNB_SEARCH_STRATEGY, reinterpret_cast(&mip_settings.bnb_search_strategy), CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST, CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING, CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST} + {CUOPT_BNB_SEARCH_STRATEGY, reinterpret_cast(&mip_settings.bnb_search_strategy), CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST, CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING, CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING} }; // Bool parameters diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index 28f593f821..6e8cc931d0 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -58,7 +58,9 @@ void test_miplib_file(result_map_t test_instance, mip_solver_settings_t solution = solve_mip(&handle_, problem, settings); - EXPECT_EQ(solution.get_termination_status(), mip_termination_status_t::FeasibleFound); + bool is_feasible = solution.get_termination_status() == mip_termination_status_t::FeasibleFound || + solution.get_termination_status() == mip_termination_status_t::Optimal; + EXPECT_TRUE(is_feasible); double obj_val = solution.get_objective_value(); // for now keep a 100% error rate EXPECT_NEAR(test_instance.cost, obj_val, test_instance.cost); From a45672abbdcc59138f6172308869f3dcbe9cb760 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 1 Sep 2025 14:35:29 +0200 Subject: [PATCH 31/44] fix incorrect solution callback --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index f5fbae682d..94fc513d21 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -941,7 +941,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut compute_user_objective(original_lp_, root_objective_), toc(stats_.start_time)); if (settings_.set_simplex_solution_callback != nullptr) { - settings_.set_simplex_solution_callback(solution.x, solution.objective); + settings_.solution_callback(solution.x, solution.objective); } if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); From ec18bd3d8b2890d2a5d3cf7c3c0741a4c28c0877 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 1 Sep 2025 15:15:57 +0200 Subject: [PATCH 32/44] fix incorrect callback --- cpp/src/dual_simplex/branch_and_bound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 94fc513d21..44ca879a83 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -940,7 +940,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut settings_.log.printf("Optimal solution found at root node. Objective %.16e. Time %.2f.\n", compute_user_objective(original_lp_, root_objective_), toc(stats_.start_time)); - if (settings_.set_simplex_solution_callback != nullptr) { + if (settings_.solution_callback != nullptr) { settings_.solution_callback(solution.x, solution.objective); } if (settings_.heuristic_preemption_callback != nullptr) { From 73fac2c44d8b990571e411fa71c964132e547acf Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 1 Sep 2025 16:22:24 +0200 Subject: [PATCH 33/44] set dfs in submip Signed-off-by: nicolas --- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 8be8e88a9b..9f3c843887 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -101,7 +101,8 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; - branch_and_bound_settings.solution_callback = [this](std::vector& solution, + branch_and_bound_settings.bnb_search_strategy = bnb_search_strategy_t::DEPTH_FIRST; + branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); }; From ea8e20e122aeb033e9ca1fd64d12a3d6d980abc5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 10 Sep 2025 10:48:43 +0200 Subject: [PATCH 34/44] small fixes based on the feedback. --- cpp/src/dual_simplex/branch_and_bound.cpp | 6 ++---- cpp/src/dual_simplex/pseudo_costs.hpp | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 44ca879a83..93358d9275 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -528,7 +528,6 @@ mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* n { logger_t log; log.log = false; - f_t gap = upper_bound - lower_bound; std::vector& leaf_vstatus = node_ptr->vstatus; lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); @@ -599,6 +598,7 @@ mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* n constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { bool send_solution = false; + f_t gap = upper_bound - lower_bound; mutex_upper_.lock(); if (leaf_objective < upper_bound_) { @@ -949,7 +949,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::OPTIMAL; } - pc_.initialize(original_lp_.num_cols); + pc_.resize(original_lp_.num_cols); strong_branching(original_lp_, settings_, stats_.start_time, @@ -1003,8 +1003,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.bnb_search_strategy == bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - // TODO: Instead of waiting, we should send a signal that we are already done in the main - // thread diving_thread.get(); } } diff --git a/cpp/src/dual_simplex/pseudo_costs.hpp b/cpp/src/dual_simplex/pseudo_costs.hpp index 444e8c7ac5..d26fe8d489 100644 --- a/cpp/src/dual_simplex/pseudo_costs.hpp +++ b/cpp/src/dual_simplex/pseudo_costs.hpp @@ -39,7 +39,7 @@ class pseudo_costs_t { void update_pseudo_costs(mip_node_t* node_ptr, f_t leaf_objective); - void initialize(i_t num_variables) + void resize(i_t num_variables) { pseudo_cost_sum_down.resize(num_variables); pseudo_cost_sum_up.resize(num_variables); From 1d2f0a0112e749c1886dfafcce999fadd6bbdb5f Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Sep 2025 17:34:00 +0200 Subject: [PATCH 35/44] removed external search_strategy. removed logs from depth-first Signed-off-by: nicolas --- .../linear_programming/cuopt/run_mip.cpp | 43 ++---- .../cuopt/linear_programming/constants.h | 6 - .../mip/solver_settings.hpp | 8 - cpp/src/dual_simplex/branch_and_bound.cpp | 146 +++++++----------- .../dual_simplex/simplex_solver_settings.hpp | 10 +- cpp/src/math_optimization/solver_settings.cu | 3 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 2 +- cpp/src/mip/solver.cu | 1 - cpp/tests/mip/miplib_test.cu | 20 +-- 9 files changed, 76 insertions(+), 163 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 2c98e4de65..0a7b9d5931 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -151,8 +151,7 @@ int run_single_file(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, - double time_limit, - cuopt::linear_programming::bnb_search_strategy_t search_strategy) + double time_limit) { const raft::handle_t handle_{}; cuopt::linear_programming::mip_solver_settings_t settings; @@ -169,8 +168,6 @@ int run_single_file(std::string file_path, } } - settings.bnb_search_strategy = search_strategy; - constexpr bool input_mps_strict = false; cuopt::mps_parser::mps_data_model_t mps_data_model; bool parsing_failed = false; @@ -255,8 +252,7 @@ void run_single_file_mp(std::string file_path, int num_cpu_threads, bool write_log_file, bool log_to_console, - double time_limit, - cuopt::linear_programming::bnb_search_strategy_t search_strategy) + double time_limit) { std::cout << "running file " << file_path << " on gpu : " << device << std::endl; auto memory_resource = make_async(); @@ -270,8 +266,7 @@ void run_single_file_mp(std::string file_path, num_cpu_threads, write_log_file, log_to_console, - time_limit, - search_strategy); + time_limit); // this is a bad design to communicate the result but better than adding complexity of IPC or // pipes exit(sol_found); @@ -345,10 +340,6 @@ int main(int argc, char* argv[]) .scan<'g', double>() .default_value(std::numeric_limits::max()); - program.add_argument("--search-strategy") - .help("Search strategy used in B&B (bfs/bfs-diving/dfs)") - .default_value(std::string("bfs")); - // Parse arguments try { program.parse_args(argc, argv); @@ -371,24 +362,10 @@ int main(int argc, char* argv[]) std::string result_file; int batch_num = -1; - bool heuristics_only = program.get("--heuristics-only")[0] == 't'; - int num_cpu_threads = program.get("--num-cpu-threads"); - bool write_log_file = program.get("--write-log-file")[0] == 't'; - bool log_to_console = program.get("--log-to-console")[0] == 't'; - std::string search_strategy_cli = program.get("--search-strategy"); - - cuopt::linear_programming::bnb_search_strategy_t search_strategy; - if (search_strategy_cli == "bfs") { - search_strategy = cuopt::linear_programming::bnb_search_strategy_t::BEST_FIRST; - } else if (search_strategy_cli == "bfs-diving") { - search_strategy = - cuopt::linear_programming::bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING; - } else if (search_strategy_cli == "dfs") { - search_strategy = cuopt::linear_programming::bnb_search_strategy_t::DEPTH_FIRST; - } else { - std::cerr << "Invalid search strategy: " << search_strategy_cli << std::endl; - exit(1); - } + bool heuristics_only = program.get("--heuristics-only")[0] == 't'; + int num_cpu_threads = program.get("--num-cpu-threads"); + bool write_log_file = program.get("--write-log-file")[0] == 't'; + bool log_to_console = program.get("--log-to-console")[0] == 't'; if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); @@ -473,8 +450,7 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, - time_limit, - search_strategy); + time_limit); } else if (sys_pid < 0) { std::cerr << "Fork failed!" << std::endl; exit(1); @@ -503,8 +479,7 @@ int main(int argc, char* argv[]) num_cpu_threads, write_log_file, log_to_console, - time_limit, - search_strategy); + time_limit); } return 0; diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 9797575417..72d05a6adb 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -62,7 +62,6 @@ #define CUOPT_SOLUTION_FILE "solution_file" #define CUOPT_NUM_CPU_THREADS "num_cpu_threads" #define CUOPT_USER_PROBLEM_FILE "user_problem_file" -#define CUOPT_BNB_SEARCH_STRATEGY "bnb_search_strategy" /* @brief LP/MIP termination status constants */ #define CUOPT_TERIMINATION_STATUS_NO_TERMINATION 0 @@ -116,9 +115,4 @@ #define CUOPT_OUT_OF_MEMORY 5 #define CUOPT_RUNTIME_ERROR 6 -/* @brief BNB search strategy constants */ -#define CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST 0 -#define CUOPT_BNB_SEARCH_STRATEGY_DEPTH_FIRST 1 -#define CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING 2 - #endif // CUOPT_CONSTANTS_H diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index a7d30baff2..4d4d29eaf9 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -35,12 +35,6 @@ struct benchmark_info_t { double objective_of_initial_population = std::numeric_limits::max(); }; -enum class bnb_search_strategy_t { - BEST_FIRST = 0, - DEPTH_FIRST = 1, - MULTITHREADED_BEST_FIRST_WITH_DIVING = 2, -}; - // Forward declare solver_settings_t for friend class template class solver_settings_t; @@ -85,8 +79,6 @@ class mip_solver_settings_t { f_t relative_mip_gap = 1.0e-4; }; - bnb_search_strategy_t bnb_search_strategy = bnb_search_strategy_t::BEST_FIRST; - /** * @brief Get the tolerance settings as a single structure. */ diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 93358d9275..689a05da1a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -472,52 +472,6 @@ void branch_and_bound_t::branch(mip_node_t* parent_node, std::move(up_child)); // child pointers moved into the tree } -template -mip_status_t branch_and_bound_t::solve_root_relaxation() -{ - settings_.log.printf("Solving LP root relaxation\n"); - simplex_solver_settings_t lp_settings = settings_; - lp_settings.inside_mip = 1; - lp_status_t root_status = solve_linear_program_advanced( - original_lp_, stats_.start_time, lp_settings, root_relax_soln_, root_vstatus_, edge_norms_); - stats_.total_lp_solve_time = toc(stats_.start_time); - assert(root_vstatus_.size() == original_lp_.num_cols); - if (root_status == lp_status_t::INFEASIBLE) { - settings_.log.printf("MIP Infeasible\n"); - if (settings_.heuristic_preemption_callback != nullptr) { - settings_.heuristic_preemption_callback(); - } - return mip_status_t::INFEASIBLE; - } - if (root_status == lp_status_t::UNBOUNDED) { - settings_.log.printf("MIP Unbounded\n"); - if (settings_.heuristic_preemption_callback != nullptr) { - settings_.heuristic_preemption_callback(); - } - return mip_status_t::UNBOUNDED; - } - if (root_status == lp_status_t::TIME_LIMIT) { - settings_.log.printf("Hit time limit\n"); - return mip_status_t::TIME_LIMIT; - } - set_uninitialized_steepest_edge_norms(original_lp_.num_cols, edge_norms_); - - std::vector fractional; - - root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); - if (settings_.set_simplex_solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem_, original_lp_, root_relax_soln_.x, original_x); - settings_.set_simplex_solution_callback(original_x, - compute_user_objective(original_lp_, root_objective_)); - } - mutex_lower_.lock(); - lower_bound_ = root_objective_; - mutex_lower_.unlock(); - - return mip_status_t::UNSET; -} - template mip_status_t branch_and_bound_t::solve_node_lp(mip_node_t* node_ptr, lp_problem_t& leaf_problem, @@ -747,7 +701,7 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, } if (toc(stats_.start_time) > settings_.time_limit) { - settings_.log.printf("Hit time limit. Stoppping\n"); + settings_.log.printf("Hit time limit. Stopping\n"); status = mip_status_t::TIME_LIMIT; break; } @@ -804,12 +758,9 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t settings_.absolute_mip_gap_tol && - relative_gap(get_upper_bound(), lower_bound) > settings_.relative_mip_gap_tol && - node_stack.size() > 0) { + while (node_stack.size() > 0) { repair_heuristic_solutions(lower_bound, solution); // Get a node off the stack @@ -817,39 +768,20 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_tdepth; - - f_t now = toc(stats_.start_time); - f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); + f_t upper_bound = get_upper_bound(); + lower_bound = get_lower_bound(); + gap = upper_bound - lower_bound; - if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { - if ((nodes_explored % 1000 == 0 || gap < 10 * settings_.absolute_mip_gap_tol || - nodes_explored < 1000) && - (time_since_log >= 1) || - (time_since_log > 60) || now > settings_.time_limit) { - settings_.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - node_stack.size(), - compute_user_objective(original_lp_, upper_bound), - compute_user_objective(original_lp_, lower_bound), - leaf_depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp_, upper_bound), - compute_user_objective(original_lp_, lower_bound)) - .c_str(), - now); - last_log = tic(); - } + if (gap < settings_.absolute_mip_gap_tol && + relative_gap(get_upper_bound(), lower_bound) < settings_.relative_mip_gap_tol) { + std::vector*> stack; + node_ptr->set_status(node_status_t::FATHOMED, stack); + graphviz_node(settings_, node_ptr, "cutoff", node_ptr->lower_bound); + remove_fathomed_nodes(stack); + continue; } if (toc(stats_.start_time) > settings_.time_limit) { - if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { - settings_.log.printf("Hit time limit. Stoppping\n"); - } - status = mip_status_t::TIME_LIMIT; break; } @@ -877,7 +809,7 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t::solve(mip_solution_t& solut } root_relax_soln_.resize(original_lp_.num_rows, original_lp_.num_cols); - mip_status_t root_status = solve_root_relaxation(); - if (root_status != mip_status_t::UNSET) { return root_status; } + + settings_.log.printf("Solving LP root relaxation\n"); + simplex_solver_settings_t lp_settings = settings_; + lp_settings.inside_mip = 1; + lp_status_t root_status = solve_linear_program_advanced( + original_lp_, stats_.start_time, lp_settings, root_relax_soln_, root_vstatus_, edge_norms_); + stats_.total_lp_solve_time = toc(stats_.start_time); + assert(root_vstatus_.size() == original_lp_.num_cols); + if (root_status == lp_status_t::INFEASIBLE) { + settings_.log.printf("MIP Infeasible\n"); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } + return mip_status_t::INFEASIBLE; + } + if (root_status == lp_status_t::UNBOUNDED) { + settings_.log.printf("MIP Unbounded\n"); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } + return mip_status_t::UNBOUNDED; + } + if (root_status == lp_status_t::TIME_LIMIT) { + settings_.log.printf("Hit time limit\n"); + return mip_status_t::TIME_LIMIT; + } + set_uninitialized_steepest_edge_norms(original_lp_.num_cols, edge_norms_); + + root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); + if (settings_.set_simplex_solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem_, original_lp_, root_relax_soln_.x, original_x); + settings_.set_simplex_solution_callback(original_x, + compute_user_objective(original_lp_, root_objective_)); + } + mutex_lower_.lock(); + lower_bound_ = root_objective_; + mutex_lower_.unlock(); std::vector fractional; const i_t num_fractional = @@ -972,10 +940,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut stats_.nodes_unexplored = 0; stats_.num_nodes = 1; - if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + if (settings_.bnb_search_strategy == search_strategy_t::DEPTH_FIRST) { settings_.log.printf("Using depth first search\n"); } else if (settings_.bnb_search_strategy == - bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { + search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { settings_.log.printf("Using best first search with diving\n"); } else { settings_.log.printf("Using best first search\n"); @@ -989,20 +957,18 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut currently_branching_ = true; mutex_branching_.unlock(); - if (settings_.bnb_search_strategy == bnb_search_strategy_t::DEPTH_FIRST) { + if (settings_.bnb_search_strategy == search_strategy_t::DEPTH_FIRST) { status = dive(branch_var, solution); } else { std::future diving_thread; - if (settings_.bnb_search_strategy == - bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { + if (settings_.bnb_search_strategy == search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { diving_thread = std::async(std::launch::async, [&]() { return dive(branch_var, solution); }); } status = explore_tree(branch_var, solution); - if (settings_.bnb_search_strategy == - bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { + if (settings_.bnb_search_strategy == search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { diving_thread.get(); } } diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 5f8fcce7fe..a685c71f49 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -29,6 +29,12 @@ namespace cuopt::linear_programming::dual_simplex { +enum class search_strategy_t { + BEST_FIRST = 0, + DEPTH_FIRST = 1, + MULTITHREADED_BEST_FIRST_WITH_DIVING = 2, +}; + template struct simplex_solver_settings_t { public: @@ -115,8 +121,8 @@ struct simplex_solver_settings_t { std::atomic* concurrent_halt; // if nullptr ignored, if !nullptr, 0 if solver should // continue, 1 if solver should halt - bnb_search_strategy_t bnb_search_strategy = - bnb_search_strategy_t::BEST_FIRST; // Search strategy for B&B + search_strategy_t bnb_search_strategy = + search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING; // Search strategy for B&B }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index b9c147801c..a049d0d09b 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -90,8 +90,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_ITERATION_LIMIT, &pdlp_settings.iteration_limit, 0, std::numeric_limits::max(), std::numeric_limits::max()}, {CUOPT_PDLP_SOLVER_MODE, reinterpret_cast(&pdlp_settings.pdlp_solver_mode), CUOPT_PDLP_SOLVER_MODE_STABLE1, CUOPT_PDLP_SOLVER_MODE_FAST1, CUOPT_PDLP_SOLVER_MODE_STABLE2}, {CUOPT_METHOD, reinterpret_cast(&pdlp_settings.method), CUOPT_METHOD_CONCURRENT, CUOPT_METHOD_DUAL_SIMPLEX, CUOPT_METHOD_CONCURRENT}, - {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1}, - {CUOPT_BNB_SEARCH_STRATEGY, reinterpret_cast(&mip_settings.bnb_search_strategy), CUOPT_BNB_SEARCH_STRATEGY_BEST_FIRST, CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING, CUOPT_BNB_SEARCH_STRATEGY_MULTITHREADED_BEST_FIRST_WITH_DIVING} + {CUOPT_NUM_CPU_THREADS, &mip_settings.num_cpu_threads, -1, std::numeric_limits::max(), -1} }; // Bool parameters diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 9f3c843887..279f7533c8 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -101,7 +101,7 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; - branch_and_bound_settings.bnb_search_strategy = bnb_search_strategy_t::DEPTH_FIRST; + branch_and_bound_settings.bnb_search_strategy = dual_simplex::search_strategy_t::DEPTH_FIRST; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index e7b0d3f94a..0f2117991f 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -168,7 +168,6 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; - branch_and_bound_settings.bnb_search_strategy = context.settings.bnb_search_strategy; if (context.settings.num_cpu_threads != -1) { branch_and_bound_settings.num_threads = std::max(1, context.settings.num_cpu_threads); diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index 6e8cc931d0..bb133d6a2b 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -16,6 +16,7 @@ */ #include "../linear_programming/utilities/pdlp_test_utilities.cuh" +#include "cuopt/linear_programming/dual_simplex/simplex_solver_settings.hpp" #include "cuopt/linear_programming/mip/solver_settings.hpp" #include "mip_utils.cuh" @@ -78,23 +79,4 @@ TEST(mip_solve, run_small_tests) } } -TEST(mip_solve, bnb_search_strategy) -{ - mip_solver_settings_t settings; - std::vector test_instances = { - {"mip/50v-10.mps", 11311031.}, {"mip/neos5.mps", 15.}, {"mip/swath1.mps", 1300.}}; - - std::vector strategies = { - bnb_search_strategy_t::BEST_FIRST, - bnb_search_strategy_t::DEPTH_FIRST, - bnb_search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING}; - - for (const auto& search : strategies) { - for (const auto& test_instance : test_instances) { - settings.bnb_search_strategy = search; - test_miplib_file(test_instance, settings); - } - } -} - } // namespace cuopt::linear_programming::test From 56a061df31b2a914d6b28ed7364b5d37d06f8a36 Mon Sep 17 00:00:00 2001 From: nicolas Date: Mon, 15 Sep 2025 18:58:23 +0200 Subject: [PATCH 36/44] small fix --- cpp/tests/mip/miplib_test.cu | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index bb133d6a2b..d62e198111 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -16,7 +16,6 @@ */ #include "../linear_programming/utilities/pdlp_test_utilities.cuh" -#include "cuopt/linear_programming/dual_simplex/simplex_solver_settings.hpp" #include "cuopt/linear_programming/mip/solver_settings.hpp" #include "mip_utils.cuh" From 7b713d85b282a3ab3e3205ba74b78b944259e116 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Sep 2025 16:08:59 +0200 Subject: [PATCH 37/44] readded validation tests for different search strategies --- cpp/tests/mip/miplib_test.cu | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index d62e198111..af5a252495 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -17,6 +17,8 @@ #include "../linear_programming/utilities/pdlp_test_utilities.cuh" #include "cuopt/linear_programming/mip/solver_settings.hpp" +#include "dual_simplex/branch_and_bound.hpp" +#include "dual_simplex/simplex_solver_settings.hpp" #include "mip_utils.cuh" #include @@ -68,6 +70,59 @@ void test_miplib_file(result_map_t test_instance, mip_solver_settings_t& problem, + const rmm::device_uvector& solution, + double integrality_tolerance) +{ + const double* lower_bound_ptr = problem.get_variable_lower_bounds().data(); + const double* upper_bound_ptr = problem.get_variable_upper_bounds().data(); + auto host_assignment = cuopt::host_copy(solution); + double* assignment_ptr = host_assignment.data(); + cuopt_assert(host_assignment.size() == problem.get_variable_lower_bounds().size(), ""); + cuopt_assert(host_assignment.size() == problem.get_variable_upper_bounds().size(), ""); + std::vector indices(host_assignment.size()); + + std::iota(indices.begin(), indices.end(), 0); + bool result = std::all_of(indices.begin(), indices.end(), [=](int idx) { + bool res = true; + if (lower_bound_ptr != nullptr) { + res = res && (assignment_ptr[idx] >= lower_bound_ptr[idx] - integrality_tolerance); + } + if (upper_bound_ptr != nullptr) { + res = res && (assignment_ptr[idx] <= upper_bound_ptr[idx] + integrality_tolerance); + } + return res; + }); + EXPECT_TRUE(result); +} + +void test_branch_and_bound_file(result_map_t test_instance, + dual_simplex::simplex_solver_settings_t settings) +{ + auto path = make_path_absolute(test_instance.file); + cuopt::mps_parser::mps_data_model_t problem = + cuopt::mps_parser::parse_mps(path, false); + + // set the time limit depending on we are in assert mode or not +#ifdef ASSERT_MODE + constexpr double test_time_limit = 60.; +#else + constexpr double test_time_limit = 30.; +#endif + + settings.time_limit = test_time_limit; + mip_solution_t solution; + + dual_simplex::branch_and_bound_t branch_and_bound(problem, settings); + dual_simplex::mip_status_t status = branch_and_bound.solve(solution); + bool is_feasible = solution.get_termination_status() == mip_termination_status_t::FeasibleFound || + solution.get_termination_status() == mip_termination_status_t::Optimal; + EXPECT_TRUE(is_feasible); + double obj_val = solution.get_objective_value(); + EXPECT_NEAR(test_instance.cost, obj_val, test_instance.cost); + test_variable_bounds(problem, solution.get_solution(), 1.0e-5); +} + TEST(mip_solve, run_small_tests) { mip_solver_settings_t settings; @@ -78,4 +133,22 @@ TEST(mip_solve, run_small_tests) } } +TEST(mip_solve, branch_and_bound_test) +{ + dual_simplex::simplex_solver_settings_t settings; + std::vector test_instances = { + {"mip/50v-10.mps", 11311031.}, {"mip/neos5.mps", 15.}, {"mip/swath1.mps", 1300.}}; + + std::vector search_strategies = { + dual_simplex::search_strategy_t::BEST_FIRST, + dual_simplex::search_strategy_t::DEPTH_FIRST, + dual_simplex::search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING}; + + for (const auto& search_strategy : search_strategies) { + settings.bnb_search_strategy = search_strategy; + for (const auto& test_instance : test_instances) { + test_branch_and_bound_file(test_instance, settings); + } + } +} } // namespace cuopt::linear_programming::test From 3026dbe8d83a5d87053cd0fcd3e9e5211e501f8f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 16 Sep 2025 19:44:28 +0200 Subject: [PATCH 38/44] Update solve.cpp --- cpp/tests/dual_simplex/unit_tests/solve.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/tests/dual_simplex/unit_tests/solve.cpp b/cpp/tests/dual_simplex/unit_tests/solve.cpp index 61bb04dee4..964ea314cd 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve.cpp +++ b/cpp/tests/dual_simplex/unit_tests/solve.cpp @@ -27,7 +27,6 @@ #include #include -#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex::test { From 895c37230165c6b902e54b911ddd3095966d7852 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 16 Sep 2025 19:45:06 +0200 Subject: [PATCH 39/44] Update solve.hpp --- cpp/src/dual_simplex/solve.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/dual_simplex/solve.hpp b/cpp/src/dual_simplex/solve.hpp index 493d7fcde4..f20844d6e6 100644 --- a/cpp/src/dual_simplex/solve.hpp +++ b/cpp/src/dual_simplex/solve.hpp @@ -21,7 +21,6 @@ #include #include #include -#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex { From 54249793f535c951539e8fc84be9cad662f3aec7 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 16 Sep 2025 19:46:08 +0200 Subject: [PATCH 40/44] Update simplex_solver_settings.hpp --- cpp/src/dual_simplex/simplex_solver_settings.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index a685c71f49..76cd7dd175 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -17,7 +17,6 @@ #pragma once -#include #include #include From f9fb5ade7b65fd2daf68ad1f09c41f8765180785 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 16 Sep 2025 19:47:14 +0200 Subject: [PATCH 41/44] Update solve.cpp --- cpp/src/dual_simplex/solve.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 83819578f8..e665bae973 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -35,7 +35,6 @@ #include #include #include -#include "cuopt/linear_programming/mip/solver_settings.hpp" namespace cuopt::linear_programming::dual_simplex { From 041e1043ef10eec25f7819a4c7229756b3306436 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 16 Sep 2025 21:12:04 +0200 Subject: [PATCH 42/44] removed validation tests as it was missing important steps --- cpp/tests/mip/miplib_test.cu | 71 ------------------------------------ 1 file changed, 71 deletions(-) diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index af5a252495..d0866455fa 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -70,59 +70,6 @@ void test_miplib_file(result_map_t test_instance, mip_solver_settings_t& problem, - const rmm::device_uvector& solution, - double integrality_tolerance) -{ - const double* lower_bound_ptr = problem.get_variable_lower_bounds().data(); - const double* upper_bound_ptr = problem.get_variable_upper_bounds().data(); - auto host_assignment = cuopt::host_copy(solution); - double* assignment_ptr = host_assignment.data(); - cuopt_assert(host_assignment.size() == problem.get_variable_lower_bounds().size(), ""); - cuopt_assert(host_assignment.size() == problem.get_variable_upper_bounds().size(), ""); - std::vector indices(host_assignment.size()); - - std::iota(indices.begin(), indices.end(), 0); - bool result = std::all_of(indices.begin(), indices.end(), [=](int idx) { - bool res = true; - if (lower_bound_ptr != nullptr) { - res = res && (assignment_ptr[idx] >= lower_bound_ptr[idx] - integrality_tolerance); - } - if (upper_bound_ptr != nullptr) { - res = res && (assignment_ptr[idx] <= upper_bound_ptr[idx] + integrality_tolerance); - } - return res; - }); - EXPECT_TRUE(result); -} - -void test_branch_and_bound_file(result_map_t test_instance, - dual_simplex::simplex_solver_settings_t settings) -{ - auto path = make_path_absolute(test_instance.file); - cuopt::mps_parser::mps_data_model_t problem = - cuopt::mps_parser::parse_mps(path, false); - - // set the time limit depending on we are in assert mode or not -#ifdef ASSERT_MODE - constexpr double test_time_limit = 60.; -#else - constexpr double test_time_limit = 30.; -#endif - - settings.time_limit = test_time_limit; - mip_solution_t solution; - - dual_simplex::branch_and_bound_t branch_and_bound(problem, settings); - dual_simplex::mip_status_t status = branch_and_bound.solve(solution); - bool is_feasible = solution.get_termination_status() == mip_termination_status_t::FeasibleFound || - solution.get_termination_status() == mip_termination_status_t::Optimal; - EXPECT_TRUE(is_feasible); - double obj_val = solution.get_objective_value(); - EXPECT_NEAR(test_instance.cost, obj_val, test_instance.cost); - test_variable_bounds(problem, solution.get_solution(), 1.0e-5); -} - TEST(mip_solve, run_small_tests) { mip_solver_settings_t settings; @@ -133,22 +80,4 @@ TEST(mip_solve, run_small_tests) } } -TEST(mip_solve, branch_and_bound_test) -{ - dual_simplex::simplex_solver_settings_t settings; - std::vector test_instances = { - {"mip/50v-10.mps", 11311031.}, {"mip/neos5.mps", 15.}, {"mip/swath1.mps", 1300.}}; - - std::vector search_strategies = { - dual_simplex::search_strategy_t::BEST_FIRST, - dual_simplex::search_strategy_t::DEPTH_FIRST, - dual_simplex::search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING}; - - for (const auto& search_strategy : search_strategies) { - settings.bnb_search_strategy = search_strategy; - for (const auto& test_instance : test_instances) { - test_branch_and_bound_file(test_instance, settings); - } - } -} } // namespace cuopt::linear_programming::test From 413ee246875000db20f7bc2b279028ae52992c88 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 19 Sep 2025 14:06:45 +0200 Subject: [PATCH 43/44] removed the search strategy toggle. further code simplifications. --- cpp/src/dual_simplex/branch_and_bound.cpp | 385 ++++++++---------- cpp/src/dual_simplex/branch_and_bound.hpp | 27 +- cpp/src/dual_simplex/mip_node.hpp | 2 - .../dual_simplex/simplex_solver_settings.hpp | 9 - cpp/src/mip/diversity/recombiners/sub_mip.cuh | 1 - 5 files changed, 203 insertions(+), 221 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 0bf82edc86..3d41921054 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -126,7 +126,7 @@ bool check_guess(const lp_problem_t& original_lp, } template -void set_uninitialized_steepest_edge_norms(i_t n, std::vector& edge_norms) +void set_uninitialized_steepest_edge_norms(std::vector& edge_norms) { for (i_t j = 0; j < edge_norms.size(); ++j) { if (edge_norms[j] <= 0.0) { edge_norms[j] = 1e-4; } @@ -187,6 +187,37 @@ dual::status_t convert_lp_status_to_dual_status(lp_status_t status) } } +template +f_t sgn(f_t x) +{ + return x < 0 ? -1 : 1; +} + +template +f_t relative_gap(f_t obj_value, f_t lower_bound) +{ + f_t user_mip_gap = obj_value == 0.0 + ? (lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) + : std::abs(obj_value - lower_bound) / std::abs(obj_value); + // Handle NaNs (i.e., NaN != NaN) + if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } + return user_mip_gap; +} + +template +std::string user_mip_gap(f_t obj_value, f_t lower_bound) +{ + const f_t user_mip_gap = relative_gap(obj_value, lower_bound); + if (user_mip_gap == std::numeric_limits::infinity()) { + return " - "; + } else { + constexpr int BUFFER_LEN = 32; + char buffer[BUFFER_LEN]; + snprintf(buffer, BUFFER_LEN - 1, "%4.1f%%", user_mip_gap * 100); + return std::string(buffer); + } +} + } // namespace template @@ -235,37 +266,6 @@ f_t branch_and_bound_t::get_lower_bound() return lower_bound; } -template -f_t sgn(f_t x) -{ - return x < 0 ? -1 : 1; -} - -template -f_t relative_gap(f_t obj_value, f_t lower_bound) -{ - f_t user_mip_gap = obj_value == 0.0 - ? (lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) - : std::abs(obj_value - lower_bound) / std::abs(obj_value); - // Handle NaNs (i.e., NaN != NaN) - if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } - return user_mip_gap; -} - -template -std::string user_mip_gap(f_t obj_value, f_t lower_bound) -{ - const f_t user_mip_gap = relative_gap(obj_value, lower_bound); - if (user_mip_gap == std::numeric_limits::infinity()) { - return " - "; - } else { - constexpr int BUFFER_LEN = 32; - char buffer[BUFFER_LEN]; - snprintf(buffer, BUFFER_LEN - 1, "%4.1f%%", user_mip_gap * 100); - return std::string(buffer); - } -} - template void branch_and_bound_t::set_new_solution(const std::vector& solution) { @@ -304,20 +304,19 @@ void branch_and_bound_t::set_new_solution(const std::vector& solu mutex_upper_.unlock(); if (is_feasible) { - mutex_lower_.lock(); - f_t lower_bound = lower_bound_; - mutex_lower_.unlock(); mutex_branching_.lock(); bool currently_branching = currently_branching_; mutex_branching_.unlock(); if (currently_branching) { + f_t user_obj = compute_user_objective(original_lp_, obj); + f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); + std::string gap = user_mip_gap(user_obj, user_lower); + settings_.log.printf( "H %+13.6e %+10.6e %s %9.2f\n", - compute_user_objective(original_lp_, obj), - compute_user_objective(original_lp_, lower_bound), - user_mip_gap(compute_user_objective(original_lp_, obj), - compute_user_objective(original_lp_, lower_bound)) - .c_str(), + user_obj, + user_lower, + gap.c_str(), toc(stats_.start_time)); } else { settings_.log.printf("New solution from primal heuristics. Objective %+.6e. Time %.2f\n", @@ -397,8 +396,7 @@ bool branch_and_bound_t::repair_solution(const std::vector& edge_ } template -void branch_and_bound_t::repair_heuristic_solutions(f_t lower_bound, - mip_solution_t& solution) +void branch_and_bound_t::repair_heuristic_solutions() { // Check if there are any solutions to repair std::vector> to_repair; @@ -423,15 +421,14 @@ void branch_and_bound_t::repair_heuristic_solutions(f_t lower_bound, upper_bound_ = repaired_obj; incumbent_.set_incumbent_solution(repaired_obj, repaired_solution); - f_t obj = compute_user_objective(original_lp_, repaired_obj); - f_t lower = compute_user_objective(original_lp_, lower_bound); - std::string gap = user_mip_gap(obj, lower); - + f_t obj = compute_user_objective(original_lp_, repaired_obj); + f_t lower = compute_user_objective(original_lp_, get_lower_bound()); + std::string user_gap = user_mip_gap(obj, lower); settings_.log.printf( "H %+13.6e %+10.6e %s %9.2f\n", obj, lower, - gap.c_str(), + user_gap.c_str(), toc(stats_.start_time)); if (settings_.solution_callback != nullptr) { @@ -472,31 +469,71 @@ void branch_and_bound_t::branch(mip_node_t* parent_node, } template -mip_status_t branch_and_bound_t::solve_node_lp( - mip_node_t* node_ptr, - lp_problem_t& leaf_problem, - csc_matrix_t& Arow, - const std::vector& var_types, - f_t upper_bound, - f_t lower_bound, - i_t nodes_explored, - i_t unexplored_nodes) +void branch_and_bound_t::update_tree(mip_node_t* node_ptr, node_status_t status) { - logger_t log; - log.log = false; - std::vector& leaf_vstatus = node_ptr->vstatus; - lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + std::vector*> stack; + node_ptr->set_status(status, stack); + remove_fathomed_nodes(stack); +} - // Set the correct bounds for the leaf problem - leaf_problem.lower = original_lp_.lower; - leaf_problem.upper = original_lp_.upper; +template +void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, + const std::vector& leaf_solution, + i_t leaf_depth, + char symbol) +{ + bool send_solution = false; + i_t nodes_explored = stats_.nodes_explored; + i_t nodes_unexplored = stats_.nodes_unexplored; + f_t gap; - std::vector bounds_changed(original_lp_.num_cols, false); - // Technically, we can get the already strengthened bounds from the node/parent instead of - // getting it from the original problem and re-strengthening. But this requires storing - // two vectors at each node and potentially cause memory issues - node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + mutex_upper_.lock(); + if (leaf_objective < upper_bound_) { + incumbent_.set_incumbent_solution(leaf_objective, leaf_solution); + upper_bound_ = leaf_objective; + f_t lower_bound = get_lower_bound(); + gap = upper_bound_ - lower_bound; + f_t obj = compute_user_objective(original_lp_, upper_bound_); + f_t lower = compute_user_objective(original_lp_, lower_bound); + settings_.log.printf("%c%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", + symbol, + nodes_explored, + nodes_unexplored, + obj, + lower, + leaf_depth, + nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, + user_mip_gap(obj, lower).c_str(), + toc(stats_.start_time)); + + send_solution = true; + } + + if (send_solution && settings_.solution_callback != nullptr) { + std::vector original_x; + uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, original_x); + settings_.solution_callback(original_x, upper_bound_); + } + mutex_upper_.unlock(); + + if (send_solution) { + mutex_gap_.lock(); + gap_ = gap; + mutex_gap_.unlock(); + } +} +template +dual::status_t branch_and_bound_t::node_dual_simplex( + i_t leaf_id, + lp_problem_t& leaf_problem, + std::vector& leaf_vstatus, + lp_solution_t& leaf_solution, + std::vector& bounds_changed, + csc_matrix_t& Arow, + f_t upper_bound, + logger_t& log) +{ i_t node_iter = 0; assert(leaf_vstatus.size() == leaf_problem.num_cols); f_t lp_start_time = tic(); @@ -509,11 +546,10 @@ mip_status_t branch_and_bound_t::solve_node_lp( // in B&B we only have equality constraints, leave it empty for default std::vector row_sense; bool feasible = - bound_strengthening(row_sense, lp_settings, leaf_problem, Arow, var_types, bounds_changed); + bound_strengthening(row_sense, lp_settings, leaf_problem, Arow, var_types_, bounds_changed); dual::status_t lp_status = dual::status_t::DUAL_UNBOUNDED; - // If the problem is infeasible after bounds strengthening, we don't need to solve the LP if (feasible) { lp_status = dual_phase2(2, 0, @@ -524,12 +560,15 @@ mip_status_t branch_and_bound_t::solve_node_lp( leaf_solution, node_iter, leaf_edge_norms); + if (lp_status == dual::status_t::NUMERICAL) { - settings_.log.printf("Numerical issue node %d. Resolving from scratch.\n", nodes_explored); + log.printf("Numerical issue node %d. Resolving from scratch.\n", leaf_id); lp_status_t second_status = solve_linear_program_advanced( leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms); lp_status = convert_lp_status_to_dual_status(second_status); } + } else { + log.printf("Infeasible after bounds strengthening. Fathoming node %d.\n", leaf_id); } mutex_stats_.lock(); @@ -537,25 +576,52 @@ mip_status_t branch_and_bound_t::solve_node_lp( stats_.total_lp_iters += node_iter; mutex_stats_.unlock(); + return lp_status; +} + +template +mip_status_t branch_and_bound_t::solve_node_lp( + mip_node_t* node_ptr, + lp_problem_t& leaf_problem, + csc_matrix_t& Arow, + const std::vector& var_types, + f_t upper_bound) +{ + logger_t log; + log.log = false; + std::vector& leaf_vstatus = node_ptr->vstatus; + lp_solution_t leaf_solution(leaf_problem.num_rows, leaf_problem.num_cols); + + // Set the correct bounds for the leaf problem + leaf_problem.lower = original_lp_.lower; + leaf_problem.upper = original_lp_.upper; + + std::vector bounds_changed(original_lp_.num_cols, false); + // Technically, we can get the already strengthened bounds from the node/parent instead of + // getting it from the original problem and re-strengthening. But this requires storing + // two vectors at each node and potentially cause memory issues + node_ptr->get_variable_bounds(leaf_problem.lower, leaf_problem.upper, bounds_changed); + + dual::status_t lp_status = node_dual_simplex(node_ptr->node_id, + leaf_problem, + leaf_vstatus, + leaf_solution, + bounds_changed, + Arow, + upper_bound, + settings_.log); + if (lp_status == dual::status_t::DUAL_UNBOUNDED) { - if (!feasible) { - settings_.log.printf("Infeasible after bounds strengthening. Fathoming node %d.\n", - nodes_explored); - } node_ptr->lower_bound = inf; - std::vector*> stack; - node_ptr->set_status(node_status_t::INFEASIBLE, stack); graphviz_node(settings_, node_ptr, "infeasible", 0.0); - remove_fathomed_nodes(stack); + update_tree(node_ptr, node_status_t::INFEASIBLE); // Node was infeasible. Do not branch } else if (lp_status == dual::status_t::CUTOFF) { node_ptr->lower_bound = upper_bound; - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); + f_t leaf_objective = compute_objective(leaf_problem, leaf_solution.x); graphviz_node(settings_, node_ptr, "cut off", leaf_objective); - remove_fathomed_nodes(stack); + update_tree(node_ptr, node_status_t::FATHOMED); // Node was cut off. Do not branch } else if (lp_status == dual::status_t::OPTIMAL) { // LP was feasible @@ -573,46 +639,9 @@ mip_status_t branch_and_bound_t::solve_node_lp( constexpr f_t fathom_tol = 1e-5; if (leaf_fractional == 0) { - bool send_solution = false; - f_t gap = upper_bound - lower_bound; - - mutex_upper_.lock(); - if (leaf_objective < upper_bound_) { - incumbent_.set_incumbent_solution(leaf_objective, leaf_solution.x); - upper_bound_ = leaf_objective; - gap = upper_bound_ - lower_bound; - f_t obj = compute_user_objective(original_lp_, upper_bound_); - f_t lower = compute_user_objective(original_lp_, lower_bound); - settings_.log.printf("B%8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", - nodes_explored, - unexplored_nodes, - obj, - lower, - node_ptr->depth, - nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(obj, lower).c_str(), - toc(stats_.start_time)); - send_solution = true; - } - - mutex_upper_.unlock(); - - if (send_solution && settings_.solution_callback != nullptr) { - std::vector original_x; - uncrush_primal_solution(original_problem_, original_lp_, incumbent_.x, original_x); - settings_.solution_callback(original_x, upper_bound_); - } - - if (send_solution) { - mutex_gap_.lock(); - gap_ = gap; - mutex_gap_.unlock(); - } - + add_feasible_solution(leaf_objective, leaf_solution.x, node_ptr->depth, 'B'); graphviz_node(settings_, node_ptr, "integer feasible", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::INTEGER_FEASIBLE, stack); - remove_fathomed_nodes(stack); + update_tree(node_ptr, node_status_t::INTEGER_FEASIBLE); } else if (leaf_objective <= upper_bound + fathom_tol) { // Choose fractional variable to branch on @@ -623,13 +652,11 @@ mip_status_t branch_and_bound_t::solve_node_lp( assert(leaf_vstatus.size() == leaf_problem.num_cols); branch(node_ptr, branch_var, leaf_solution.x[branch_var], leaf_vstatus); - node_ptr->set_status(node_status_t::HAS_CHILDREN); + node_ptr->status = node_status_t::HAS_CHILDREN; } else { graphviz_node(settings_, node_ptr, "fathomed", leaf_objective); - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - remove_fathomed_nodes(stack); + update_tree(node_ptr, node_status_t::FATHOMED); } } else { graphviz_node(settings_, node_ptr, "numerical", 0.0); @@ -647,7 +674,7 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, { mip_status_t status = mip_status_t::UNSET; logger_t log; - log.log = false; + log.log = false; auto compare = [](mip_node_t* a, mip_node_t* b) { return a->lower_bound > b->lower_bound; // True if a comes before b, elements that come before are output last @@ -670,29 +697,26 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, csc_matrix_t Arow(1, 1, 1); original_lp_.A.transpose(Arow); - f_t lower_bound = get_lower_bound(); - f_t gap = get_upper_bound() - lower_bound; - f_t last_log = 0; - i_t nodes_explored = 0; + f_t lower_bound = get_lower_bound(); + f_t gap = get_upper_bound() - lower_bound; + f_t last_log = 0; while (gap > settings_.absolute_mip_gap_tol && relative_gap(get_upper_bound(), lower_bound) > settings_.relative_mip_gap_tol && heap.size() > 0) { - repair_heuristic_solutions(lower_bound, solution); + repair_heuristic_solutions(); // Get a node off the heap mip_node_t* node_ptr = heap.top(); heap.pop(); - nodes_explored++; + stats_.nodes_explored++; f_t upper_bound = get_upper_bound(); if (upper_bound < node_ptr->lower_bound) { // This node was put on the heap earlier but its lower bound is now greater than the current // upper bound - std::vector*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); + update_tree(node_ptr, node_status_t::FATHOMED); graphviz_node(settings_, node_ptr, "cutoff", node_ptr->lower_bound); - remove_fathomed_nodes(stack); continue; } mutex_lower_.lock(); @@ -703,22 +727,25 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, gap_ = gap = upper_bound - lower_bound; mutex_gap_.unlock(); + i_t nodes_explored = stats_.nodes_explored; f_t now = toc(stats_.start_time); f_t time_since_log = last_log == 0 ? 1.0 : toc(last_log); if ((nodes_explored % 1000 == 0 || gap < 10 * settings_.absolute_mip_gap_tol || nodes_explored < 1000) && (time_since_log >= 1) || (time_since_log > 60) || now > settings_.time_limit) { + f_t user_obj = compute_user_objective(original_lp_, upper_bound); + f_t user_lower = compute_user_objective(original_lp_, lower_bound); + std::string user_gap = user_mip_gap(user_obj, user_lower); + settings_.log.printf(" %8d %8lu %+13.6e %+10.6e %4d %7.1e %s %9.2f\n", nodes_explored, heap.size(), - compute_user_objective(original_lp_, upper_bound), - compute_user_objective(original_lp_, lower_bound), + user_obj, + user_lower, node_ptr->depth, nodes_explored > 0 ? stats_.total_lp_iters / nodes_explored : 0, - user_mip_gap(compute_user_objective(original_lp_, upper_bound), - compute_user_objective(original_lp_, lower_bound)) - .c_str(), + user_gap.c_str(), now); last_log = tic(); } @@ -729,14 +756,7 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, break; } - status = solve_node_lp(node_ptr, - leaf_problem, - Arow, - var_types_, - upper_bound, - lower_bound, - nodes_explored, - heap.size()); + status = solve_node_lp(node_ptr, leaf_problem, Arow, var_types_, upper_bound); if (status == mip_status_t::NUMERICAL) { break; } @@ -748,7 +768,6 @@ mip_status_t branch_and_bound_t::explore_tree(i_t branch_var, } stats_.nodes_unexplored = heap.size(); - stats_.nodes_explored = nodes_explored; if (stats_.nodes_unexplored == 0) { mutex_lower_.lock(); @@ -793,8 +812,6 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t 0) { - repair_heuristic_solutions(lower_bound, solution); - // Get a node off the stack mip_node_t* node_ptr = node_stack.back(); node_stack.pop_back(); @@ -806,10 +823,7 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t*> stack; - node_ptr->set_status(node_status_t::FATHOMED, stack); - graphviz_node(settings_, node_ptr, "cutoff", node_ptr->lower_bound); - remove_fathomed_nodes(stack); + update_tree(node_ptr, node_status_t::FATHOMED); continue; } @@ -818,15 +832,9 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_tstatus == node_status_t::HAS_CHILDREN) { // Martin's child selection @@ -847,22 +855,6 @@ mip_status_t branch_and_bound_t::dive(i_t branch_var, mip_solution_t::solve(mip_solution_t& solut settings_.log.printf("Hit time limit\n"); return mip_status_t::TIME_LIMIT; } - set_uninitialized_steepest_edge_norms(original_lp_.num_cols, edge_norms_); + set_uninitialized_steepest_edge_norms(edge_norms_); root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); - if (settings.set_simplex_solution_callback != nullptr) { + if (settings_.set_simplex_solution_callback != nullptr) { std::vector original_x; uncrush_primal_solution(original_problem_, original_lp_, root_relax_soln_.x, original_x); std::vector original_dual; @@ -932,7 +924,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.z, original_dual, original_z); - settings.set_simplex_solution_callback( + settings_.set_simplex_solution_callback( original_x, original_dual, compute_user_objective(original_lp_, root_objective_)); } mutex_lower_.lock(); @@ -989,15 +981,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut stats_.nodes_unexplored = 0; stats_.num_nodes = 1; - if (settings_.bnb_search_strategy == search_strategy_t::DEPTH_FIRST) { - settings_.log.printf("Using depth first search\n"); - } else if (settings_.bnb_search_strategy == - search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - settings_.log.printf("Using best first search with diving\n"); - } else { - settings_.log.printf("Using best first search\n"); - } - settings_.log.printf( "| Explored | Unexplored | Objective | Bound | Depth | Iter/Node | Gap | " " Time \n"); @@ -1006,21 +989,15 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut currently_branching_ = true; mutex_branching_.unlock(); - if (settings_.bnb_search_strategy == search_strategy_t::DEPTH_FIRST) { - status = dive(branch_var, solution); - } else { - std::future diving_thread; + std::future diving_thread; - if (settings_.bnb_search_strategy == search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - diving_thread = std::async(std::launch::async, [&]() { return dive(branch_var, solution); }); - } + if (settings_.num_threads > 0) { + diving_thread = std::async(std::launch::async, [&]() { return dive(branch_var, solution); }); + } - status = explore_tree(branch_var, solution); + status = explore_tree(branch_var, solution); - if (settings_.bnb_search_strategy == search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING) { - diving_thread.get(); - } - } + if (settings_.num_threads > 0) { mip_status_t diving_status = diving_thread.get(); } mutex_branching_.lock(); currently_branching_ = false; diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index e2d5b82a25..efe28000bd 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -137,8 +137,18 @@ class branch_and_bound_t { pseudo_costs_t pc_; std::mutex mutex_pc_; + // Update the status of the nodes in the search tree. + void update_tree(mip_node_t* node_ptr, node_status_t status); + + // Update the incumbent solution with the new feasible solution. + // found during branch and bound. + void add_feasible_solution(f_t leaf_objective, + const std::vector& leaf_solution, + i_t leaf_depth, + char symbol); + // Repairs low-quality solutions from the heuristics, if it is applicable. - void repair_heuristic_solutions(f_t lower_bound, mip_solution_t& solution); + void repair_heuristic_solutions(); // Explore the search tree using the best-first search strategy. mip_status_t explore_tree(i_t branch_var, mip_solution_t& solution); @@ -157,10 +167,17 @@ class branch_and_bound_t { lp_problem_t& leaf_problem, csc_matrix_t& Arow, const std::vector& var_types, - f_t upper_bound, - f_t lower_bound, - i_t nodes_explored, - i_t unexplored_nodes); + f_t upper_bound); + + // Solve the LP relaxation of a leaf node using the dual simplex method. + dual::status_t node_dual_simplex(i_t leaf_id, + lp_problem_t& leaf_problem, + std::vector& leaf_vstatus, + lp_solution_t& leaf_solution, + std::vector& bounds_changed, + csc_matrix_t& Arow, + f_t upper_bound, + logger_t& log); }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/mip_node.hpp b/cpp/src/dual_simplex/mip_node.hpp index 075cb913f7..2d315fe0e3 100644 --- a/cpp/src/dual_simplex/mip_node.hpp +++ b/cpp/src/dual_simplex/mip_node.hpp @@ -146,8 +146,6 @@ class mip_node_t { } } - void set_status(node_status_t new_status) { status = new_status; } - // outputs a stack containing inactive nodes in the tree that can be freed void set_status(node_status_t node_status, std::vector& stack) { diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 85e43b8847..5b7e8bf0fa 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -28,12 +28,6 @@ namespace cuopt::linear_programming::dual_simplex { -enum class search_strategy_t { - BEST_FIRST = 0, - DEPTH_FIRST = 1, - MULTITHREADED_BEST_FIRST_WITH_DIVING = 2, -}; - template struct simplex_solver_settings_t { public: @@ -119,9 +113,6 @@ struct simplex_solver_settings_t { mutable logger_t log; std::atomic* concurrent_halt; // if nullptr ignored, if !nullptr, 0 if solver should // continue, 1 if solver should halt - - search_strategy_t bnb_search_strategy = - search_strategy_t::MULTITHREADED_BEST_FIRST_WITH_DIVING; // Search strategy for B&B }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 436bd3459f..49ccc5e561 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -112,7 +112,6 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; - branch_and_bound_settings.bnb_search_strategy = dual_simplex::search_strategy_t::DEPTH_FIRST; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); From 53127f51c2f410351dcadfc92889aebecac74390 Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 19 Sep 2025 15:43:16 +0200 Subject: [PATCH 44/44] fixed code style Signed-off-by: nicolas --- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 49ccc5e561..1818092fc9 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -112,7 +112,7 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.absolute_mip_gap_tol = context.settings.tolerances.absolute_mip_gap; branch_and_bound_settings.relative_mip_gap_tol = context.settings.tolerances.relative_mip_gap; branch_and_bound_settings.integer_tol = context.settings.tolerances.integrality_tolerance; - branch_and_bound_settings.solution_callback = [this](std::vector& solution, + branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { this->solution_callback(solution, objective); };