From fc1ab19e71ed9245dc062f5a941b9854d32f43d4 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 2 Jun 2026 12:36:05 +0200 Subject: [PATCH 1/4] expose diving hyper parameters. fixed circular dependency between pseudo_costs, worker and diving_heuristics. Signed-off-by: Nicolas L. Guidotti --- .../cuopt/linear_programming/constants.h | 13 +++++ .../mip/diving_hyper_params.hpp | 54 ++++++++++++++++++ .../mip/solver_settings.hpp | 3 + cpp/src/branch_and_bound/branch_and_bound.cpp | 57 +++++++------------ .../branch_and_bound/diving_heuristics.cpp | 12 ++++ .../branch_and_bound/diving_heuristics.hpp | 37 ++++++++++++ cpp/src/branch_and_bound/pseudo_costs.hpp | 7 ++- cpp/src/branch_and_bound/worker.hpp | 24 ++------ .../dual_simplex/simplex_solver_settings.hpp | 27 +-------- cpp/src/math_optimization/solver_settings.cu | 12 ++++ cpp/src/mip_heuristics/solver.cu | 2 + 11 files changed, 165 insertions(+), 83 deletions(-) create mode 100644 cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 40e68eca65..a9eb652042 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -114,6 +114,19 @@ #define CUOPT_MIP_HYPER_HEURISTIC_RELATED_VARS_TIME_LIMIT \ "mip_hyper_heuristic_related_vars_time_limit" +/* @brief Diving heuristic toggles: -1 automatic, 0 disabled, 1 enabled */ +#define CUOPT_MIP_HYPER_DIVING_LINE_SEARCH "mip_hyper_diving_line_search" +#define CUOPT_MIP_HYPER_DIVING_PSEUDOCOST "mip_hyper_diving_pseudocost" +#define CUOPT_MIP_HYPER_DIVING_GUIDED "mip_hyper_diving_guided" +#define CUOPT_MIP_HYPER_DIVING_COEFFICIENT "mip_hyper_diving_coefficient" +/* @brief Diving heuristic limits */ +#define CUOPT_MIP_HYPER_DIVING_MIN_NODE_DEPTH "mip_hyper_diving_min_node_depth" +#define CUOPT_MIP_HYPER_DIVING_NODE_LIMIT "mip_hyper_diving_node_limit" +#define CUOPT_MIP_HYPER_DIVING_ITERATION_LIMIT_FACTOR "mip_hyper_diving_iteration_limit_factor" +#define CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT "mip_hyper_diving_backtrack_limit" +/* @brief Show per-strategy diving symbol in logs (true) instead of a generic 'D' */ +#define CUOPT_MIP_HYPER_DIVING_SHOW_DIVING_TYPE "mip_hyper_diving_show_diving_type" + /* @brief MIP determinism mode constants */ #define CUOPT_MODE_OPPORTUNISTIC 0 #define CUOPT_MODE_DETERMINISTIC 1 diff --git a/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp new file mode 100644 index 0000000000..a509d9ca03 --- /dev/null +++ b/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp @@ -0,0 +1,54 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +namespace cuopt::linear_programming { + +/** + * @brief Tuning knobs for the dual-simplex diving heuristics used in MIP B&B. + * + * Used directly as simplex_solver_settings_t::diving_settings and copied into + * branch_and_bound_settings.diving_settings before solve. These are registered + * in the unified parameter framework via solver_settings_t and can be loaded + * from a config file with load_parameters_from_file(). + */ +struct mip_diving_hyper_params_t { + // -1 automatic, 0 disabled, 1 enabled + int line_search_diving = -1; + int pseudocost_diving = -1; + int guided_diving = -1; + int coefficient_diving = -1; + + // The minimum depth to start diving from. + int min_node_depth = 10; + + // The maximum number of nodes when performing a dive. + int node_limit = 500; + + // The maximum number of dual simplex iteration allowed + // in a single dive. This set in terms of the total number of + // iterations in the best-first threads. + double iteration_limit_factor = 0.05; + + // The maximum backtracking allowed. + int backtrack_limit = 5; + + // If a given diving heuristic found a new incumbent, show the corresponding + // symbol in the first column of the log row. When false, every dive collapses + // to 'D'. Otherwise, + // B = best-first + // H = heuristics + // C = coefficient diving + // L = line-search diving + // P = pseudocost diving + // G = guided diving + // U = unknown + bool show_diving_type = false; +}; + +} // namespace cuopt::linear_programming diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 685b1360b8..9f9e64e7ba 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -164,6 +165,8 @@ class mip_solver_settings_t { mip_heuristics_hyper_params_t heuristic_params; + mip_diving_hyper_params_t diving_params; + private: std::vector mip_callbacks_; std::optional semi_continuous_original_num_variables_; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 0222ad6fe9..8283888442 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -213,32 +213,6 @@ std::string user_mip_gap(const lp_problem_t& lp, f_t obj_value, f_t lo } } -#ifdef SHOW_DIVING_TYPE -inline char feasible_solution_symbol(search_strategy_t strategy) -{ - switch (strategy) { - case search_strategy_t::BEST_FIRST: return 'B'; - case search_strategy_t::COEFFICIENT_DIVING: return 'C'; - case search_strategy_t::LINE_SEARCH_DIVING: return 'L'; - case search_strategy_t::PSEUDOCOST_DIVING: return 'P'; - case search_strategy_t::GUIDED_DIVING: return 'G'; - default: return 'U'; - } -} -#else -inline char feasible_solution_symbol(search_strategy_t strategy) -{ - switch (strategy) { - case search_strategy_t::BEST_FIRST: return 'B'; - case search_strategy_t::COEFFICIENT_DIVING: return 'D'; - case search_strategy_t::LINE_SEARCH_DIVING: return 'D'; - case search_strategy_t::PSEUDOCOST_DIVING: return 'D'; - case search_strategy_t::GUIDED_DIVING: return 'D'; - default: return 'U'; - } -} -#endif - } // namespace template @@ -811,15 +785,20 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, { bool send_solution = false; + const bool log_diving_type = settings_.diving_settings.show_diving_type; settings_.log.debug("%c found a feasible solution with obj=%.10e.\n", - feasible_solution_symbol(thread_type), + feasible_solution_symbol(thread_type, log_diving_type), compute_user_objective(original_lp_, leaf_objective)); mutex_upper_.lock(); if (improves_incumbent(leaf_objective)) { incumbent_.set_incumbent_solution(leaf_objective, leaf_solution); upper_bound_ = std::min(upper_bound_.load(), leaf_objective); - report(feasible_solution_symbol(thread_type), leaf_objective, get_lower_bound(), leaf_depth, 0); + report(feasible_solution_symbol(thread_type, log_diving_type), + leaf_objective, + get_lower_bound(), + leaf_depth, + 0); send_solution = true; } @@ -1761,22 +1740,24 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t settings_.bnb_steal_chance >= 0 ? settings_.bnb_steal_chance : MIP_DEFAULT_STEAL_CHANCE; node_queue_t& node_queue = worker->node_queue; - worker->calculate_num_diving_workers(bfs_worker_pool_.size(), - diving_worker_pool_.size(), - has_solver_space_incumbent(), - settings_.diving_settings); + mip_diving_hyper_params_t diving_settings = settings_.diving_settings; + if (diving_settings.guided_diving != 0 && !has_solver_space_incumbent()) { + diving_settings.guided_diving = 0; + } + + worker->calculate_num_diving_workers( + bfs_worker_pool_.size(), diving_worker_pool_.size(), diving_settings); while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && rel_gap > settings_.relative_mip_gap_tol && node_queue.best_first_queue_size() > 0) { // If the guided diving was disabled previously due to the lack of an incumbent solution, // re-enable as soon as a new incumbent is found. if (diving_worker_pool_.size() > 0 && settings_.diving_settings.guided_diving != 0 && - worker->max_diving_workers[GUIDED_DIVING] == 0) { + diving_settings.guided_diving == 0) { if (has_solver_space_incumbent()) { - worker->calculate_num_diving_workers(bfs_worker_pool_.size(), - diving_worker_pool_.size(), - has_solver_space_incumbent(), - settings_.diving_settings); + diving_settings.guided_diving = 1; + worker->calculate_num_diving_workers( + bfs_worker_pool_.size(), diving_worker_pool_.size(), diving_settings); } } @@ -3547,7 +3528,7 @@ void branch_and_bound_t::deterministic_process_worker_solutions( i_t nodes_unexplored = exploration_stats_.nodes_unexplored.load(); search_strategy_t worker_type = get_worker_type(pool, sol->worker_id); - report(feasible_solution_symbol(worker_type), + report(feasible_solution_symbol(worker_type, settings_.diving_settings.show_diving_type), sol->objective, deterministic_lower, sol->depth, diff --git a/cpp/src/branch_and_bound/diving_heuristics.cpp b/cpp/src/branch_and_bound/diving_heuristics.cpp index 4989e48c6a..46a6816795 100644 --- a/cpp/src/branch_and_bound/diving_heuristics.cpp +++ b/cpp/src/branch_and_bound/diving_heuristics.cpp @@ -126,6 +126,12 @@ branch_variable_t pseudocost_diving(pseudo_costs_t& pc, round_dir = branch_direction_t::DOWN; } + log.debug("Pseudocost diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + return {branch_var, round_dir}; } @@ -168,6 +174,12 @@ branch_variable_t guided_diving(pseudo_costs_t& pc, } } + log.debug("Guided diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + max_score); + return {branch_var, round_dir}; } diff --git a/cpp/src/branch_and_bound/diving_heuristics.hpp b/cpp/src/branch_and_bound/diving_heuristics.hpp index dfeabe3a5f..fe7b296dff 100644 --- a/cpp/src/branch_and_bound/diving_heuristics.hpp +++ b/cpp/src/branch_and_bound/diving_heuristics.hpp @@ -16,6 +16,43 @@ namespace cuopt::linear_programming::dual_simplex { +// When `log_diving_type` is true, each diving strategy gets its own letter; +// otherwise every dive collapses to 'D'. +inline char feasible_solution_symbol(search_strategy_t strategy, bool log_diving_type) +{ + if (strategy == BEST_FIRST) return 'B'; + if (!log_diving_type) { + switch (strategy) { + case COEFFICIENT_DIVING: + case LINE_SEARCH_DIVING: + case PSEUDOCOST_DIVING: + case GUIDED_DIVING: return 'D'; + default: return 'U'; + } + } + switch (strategy) { + case COEFFICIENT_DIVING: return 'C'; + case LINE_SEARCH_DIVING: return 'L'; + case PSEUDOCOST_DIVING: return 'P'; + case GUIDED_DIVING: return 'G'; + default: return 'U'; + } +} + +inline bool is_search_strategy_enabled(search_strategy_t strategy, + const mip_diving_hyper_params_t& settings) +{ + switch (strategy) { + case BEST_FIRST: return true; + case PSEUDOCOST_DIVING: return settings.pseudocost_diving != 0; + case LINE_SEARCH_DIVING: return settings.line_search_diving != 0; + case GUIDED_DIVING: return settings.guided_diving != 0; + case COEFFICIENT_DIVING: return settings.coefficient_diving != 0; + } + + return false; +} + template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, diff --git a/cpp/src/branch_and_bound/pseudo_costs.hpp b/cpp/src/branch_and_bound/pseudo_costs.hpp index a370a00de8..6fd7dc5bb6 100644 --- a/cpp/src/branch_and_bound/pseudo_costs.hpp +++ b/cpp/src/branch_and_bound/pseudo_costs.hpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -30,6 +29,12 @@ namespace cuopt::linear_programming::dual_simplex { template struct mip_symmetry_t; +template +struct branch_and_bound_worker_t; + +template +struct branch_and_bound_stats_t; + template struct reliability_branching_settings_t { // Lower bound for the maximum number of LP iterations for a single trial branching diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 29a3164dfc..0e0761e5e6 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -42,22 +43,6 @@ struct branch_and_bound_stats_t { omp_atomic_t lexical_reduction_pruned_nodes = 0; }; -template -bool is_search_strategy_enabled(search_strategy_t strategy, - bool has_incumbent, - diving_heuristics_settings_t settings) -{ - switch (strategy) { - case BEST_FIRST: return true; - case PSEUDOCOST_DIVING: return settings.pseudocost_diving != 0; - case LINE_SEARCH_DIVING: return settings.line_search_diving != 0; - case GUIDED_DIVING: return settings.guided_diving != 0 && has_incumbent; - case COEFFICIENT_DIVING: return settings.coefficient_diving != 0; - } - - return false; -} - template class branch_and_bound_worker_t { public: @@ -191,12 +176,11 @@ class bfs_worker_t : public branch_and_bound_worker_t { // of workers allows the solver to be more deterministic. void calculate_num_diving_workers(i_t num_bfs_workers, i_t total_diving_workers, - bool has_incumbent, - const diving_heuristics_settings_t& settings) + const mip_diving_hyper_params_t& settings) { i_t num_active = 0; for (i_t i = 1; i < num_search_strategies; ++i) { - num_active += is_search_strategy_enabled(search_strategies[i], has_incumbent, settings); + num_active += is_search_strategy_enabled(search_strategies[i], settings); } total_max_diving_workers = 0; @@ -204,7 +188,7 @@ class bfs_worker_t : public branch_and_bound_worker_t { if (num_active == 0) { return; } for (size_t i = 1, k = 0; i < num_search_strategies; ++i) { - if (is_search_strategy_enabled(search_strategies[i], has_incumbent, settings)) { + if (is_search_strategy_enabled(search_strategies[i], settings)) { // Calculate the number of workers for a given diving heuristic auto [type_start, type_end] = calculate_index_range(k, total_diving_workers, num_active); i_t workers_per_type = type_end - type_start; diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 27eac7f985..3bbd85daf3 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -7,6 +7,8 @@ #pragma once +#include + #include #include @@ -20,29 +22,6 @@ namespace cuopt::linear_programming::dual_simplex { -template -struct diving_heuristics_settings_t { - // -1 automatic, 0 disabled, 1 enabled - i_t line_search_diving = -1; - i_t pseudocost_diving = -1; - i_t guided_diving = -1; - i_t coefficient_diving = -1; - - // The minimum depth to start diving from. - i_t min_node_depth = 10; - - // The maximum number of nodes when performing a dive. - i_t node_limit = 500; - - // The maximum number of dual simplex iteration allowed - // in a single dive. This set in terms of the total number of - // iterations in the best-first threads. - f_t iteration_limit_factor = 0.05; - - // The maximum backtracking allowed. - i_t backtrack_limit = 5; -}; - template struct simplex_solver_settings_t { public: @@ -214,7 +193,7 @@ struct simplex_solver_settings_t { // >1 - set as the iteration limit in dual simplex i_t strong_branching_simplex_iteration_limit; - diving_heuristics_settings_t diving_settings; // Settings for the diving heuristics + mip_diving_hyper_params_t diving_settings; // Settings for the diving heuristics // In B&B, indicate the chance in which a worker can steal a node from another worker. // -1 - automatic (0.05) diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 2a7d942da5..30e04fe3a9 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -115,6 +115,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_HEURISTIC_RELAXED_LP_TIME_LIMIT, &mip_settings.heuristic_params.relaxed_lp_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(1.0), "base relaxed LP time cap in heuristics"}, {CUOPT_MIP_HYPER_HEURISTIC_RELATED_VARS_TIME_LIMIT, &mip_settings.heuristic_params.related_vars_time_limit, f_t(1e-9), std::numeric_limits::infinity(), f_t(30.0), "time for related-variable structure build"}, {CUOPT_MIP_SEMICONTINUOUS_BIG_M, &mip_settings.semi_continuous_big_m, f_t(1.0), std::numeric_limits::infinity(), f_t(1e10), "big-M value for semi-continuous variables with no finite upper bound"}, + // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_DIVING_ITERATION_LIMIT_FACTOR, &mip_settings.diving_params.iteration_limit_factor, f_t(0.0), f_t(1.0), f_t(0.05), "fraction of best-first iterations allowed per dive"}, }; // Int parameters @@ -161,6 +163,14 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_HEURISTIC_N_OF_MINIMUMS_FOR_EXIT, &mip_settings.heuristic_params.n_of_minimums_for_exit, 1, std::numeric_limits::max(), 7000, "FJ baseline local-minima exit threshold"}, {CUOPT_MIP_HYPER_HEURISTIC_ENABLED_RECOMBINERS, &mip_settings.heuristic_params.enabled_recombiners, 0, 15, 15, "bitmask: 1=BP 2=FP 4=LS 8=SubMIP"}, {CUOPT_MIP_HYPER_HEURISTIC_CYCLE_DETECTION_LENGTH, &mip_settings.heuristic_params.cycle_detection_length, 1, std::numeric_limits::max(), 30, "FP assignment cycle ring buffer length"}, + // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_DIVING_LINE_SEARCH, &mip_settings.diving_params.line_search_diving, -1, 1, -1, "line-search diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_PSEUDOCOST, &mip_settings.diving_params.pseudocost_diving, -1, 1, -1, "pseudocost diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_GUIDED, &mip_settings.diving_params.guided_diving, -1, 1, -1, "guided diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_COEFFICIENT, &mip_settings.diving_params.coefficient_diving, -1, 1, -1, "coefficient diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_MIN_NODE_DEPTH, &mip_settings.diving_params.min_node_depth, 0, std::numeric_limits::max(), 10, "minimum depth at which to start diving"}, + {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip_settings.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, + {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, }; // Bool parameters @@ -179,6 +189,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}, {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, {CUOPT_MIP_PROBING, &mip_settings.probing, true}, + // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") + {CUOPT_MIP_HYPER_DIVING_SHOW_DIVING_TYPE, &mip_settings.diving_params.show_diving_type, false, "log diving heuristic type when it finds a new incumbent"}, }; // String parameters string_parameters = { diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index c25ade0c05..187f774c37 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -393,6 +393,8 @@ solution_t mip_solver_t::run_solver() : context.settings.reduced_cost_strengthening; branch_and_bound_settings.symmetry = context.settings.symmetry; + branch_and_bound_settings.diving_settings = context.settings.diving_params; + // Set the branch and bound -> primal heuristics callback branch_and_bound_settings.solution_callback = std::bind(&branch_and_bound_solution_helper_t::solution_callback, From 2fedec890a3bec7fbe8f54a33b0be0ada0a46f15 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 2 Jun 2026 14:11:40 +0200 Subject: [PATCH 2/4] transform hyper parameters into templated objects. addressed other coderabbit comments. Signed-off-by: Nicolas L. Guidotti --- .../cuopt/linear_programming/constants.h | 2 +- .../mip/diving_hyper_params.hpp | 11 +++--- .../mip/heuristics_hyper_params.hpp | 35 ++++++++++--------- .../mip/solver_settings.hpp | 4 +-- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 ++-- .../branch_and_bound/diving_heuristics.hpp | 7 ++-- cpp/src/branch_and_bound/worker.hpp | 2 +- .../dual_simplex/simplex_solver_settings.hpp | 2 +- cpp/src/math_optimization/solver_settings.cu | 2 +- cpp/tests/mip/heuristics_hyper_params_test.cu | 2 +- 10 files changed, 39 insertions(+), 34 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index a9eb652042..ae02ee00c7 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -125,7 +125,7 @@ #define CUOPT_MIP_HYPER_DIVING_ITERATION_LIMIT_FACTOR "mip_hyper_diving_iteration_limit_factor" #define CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT "mip_hyper_diving_backtrack_limit" /* @brief Show per-strategy diving symbol in logs (true) instead of a generic 'D' */ -#define CUOPT_MIP_HYPER_DIVING_SHOW_DIVING_TYPE "mip_hyper_diving_show_diving_type" +#define CUOPT_MIP_HYPER_DIVING_SHOW_TYPE "mip_hyper_diving_show_type" /* @brief MIP determinism mode constants */ #define CUOPT_MODE_OPPORTUNISTIC 0 diff --git a/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp index a509d9ca03..bd9e316104 100644 --- a/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp @@ -17,6 +17,7 @@ namespace cuopt::linear_programming { * in the unified parameter framework via solver_settings_t and can be loaded * from a config file with load_parameters_from_file(). */ +template struct mip_diving_hyper_params_t { // -1 automatic, 0 disabled, 1 enabled int line_search_diving = -1; @@ -25,18 +26,18 @@ struct mip_diving_hyper_params_t { int coefficient_diving = -1; // The minimum depth to start diving from. - int min_node_depth = 10; + i_t min_node_depth = 10; // The maximum number of nodes when performing a dive. - int node_limit = 500; + i_t node_limit = 500; // The maximum number of dual simplex iteration allowed // in a single dive. This set in terms of the total number of // iterations in the best-first threads. - double iteration_limit_factor = 0.05; + f_t iteration_limit_factor = 0.05; // The maximum backtracking allowed. - int backtrack_limit = 5; + i_t backtrack_limit = 5; // If a given diving heuristic found a new incumbent, show the corresponding // symbol in the first column of the log row. When false, every dive collapses @@ -48,7 +49,7 @@ struct mip_diving_hyper_params_t { // P = pseudocost diving // G = guided diving // U = unknown - bool show_diving_type = false; + bool show_type = false; }; } // namespace cuopt::linear_programming diff --git a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp index c0b644544a..71bbbe169a 100644 --- a/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/heuristics_hyper_params.hpp @@ -17,24 +17,25 @@ namespace cuopt::linear_programming { * These are registered in the unified parameter framework via solver_settings_t * and can be loaded from a config file with load_parameters_from_file(). */ +template struct mip_heuristics_hyper_params_t { - int population_size = 32; // max solutions in pool - int num_cpufj_threads = 8; // parallel CPU FJ climbers - double presolve_time_ratio = 0.1; // fraction of total time for presolve - double presolve_max_time = 60.0; // hard cap on presolve seconds - double root_lp_time_ratio = 0.1; // fraction of total time for root LP - double root_lp_max_time = 15.0; // hard cap on root LP seconds - double rins_time_limit = 3.0; // per-call RINS sub-MIP time - double rins_max_time_limit = 20.0; // ceiling for RINS adaptive time budget - double rins_fix_rate = 0.5; // RINS variable fix rate - int stagnation_trigger = 3; // FP loops w/o improvement before recombination - int max_iterations_without_improvement = 8; // diversity step depth after stagnation - double initial_infeasibility_weight = 1000.0; // constraint violation penalty seed - int n_of_minimums_for_exit = 7000; // FJ baseline local-minima exit threshold - int enabled_recombiners = 15; // bitmask: 1=BP 2=FP 4=LS 8=SubMIP - int cycle_detection_length = 30; // FP assignment cycle ring buffer - double relaxed_lp_time_limit = 1.0; // base relaxed LP time cap in heuristics - double related_vars_time_limit = 30.0; // time for related-variable structure build + i_t population_size = 32; // max solutions in pool + i_t num_cpufj_threads = 8; // parallel CPU FJ climbers + f_t presolve_time_ratio = 0.1; // fraction of total time for presolve + f_t presolve_max_time = 60.0; // hard cap on presolve seconds + f_t root_lp_time_ratio = 0.1; // fraction of total time for root LP + f_t root_lp_max_time = 15.0; // hard cap on root LP seconds + f_t rins_time_limit = 3.0; // per-call RINS sub-MIP time + f_t rins_max_time_limit = 20.0; // ceiling for RINS adaptive time budget + f_t rins_fix_rate = 0.5; // RINS variable fix rate + i_t stagnation_trigger = 3; // FP loops w/o improvement before recombination + i_t max_iterations_without_improvement = 8; // diversity step depth after stagnation + f_t initial_infeasibility_weight = 1000.0; // constraint violation penalty seed + i_t n_of_minimums_for_exit = 7000; // FJ baseline local-minima exit threshold + i_t enabled_recombiners = 15; // bitmask: 1=BP 2=FP 4=LS 8=SubMIP + i_t cycle_detection_length = 30; // FP assignment cycle ring buffer + f_t relaxed_lp_time_limit = 1.0; // base relaxed LP time cap in heuristics + f_t related_vars_time_limit = 30.0; // time for related-variable structure build }; } // namespace cuopt::linear_programming diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 9f9e64e7ba..1038c29057 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -163,9 +163,9 @@ class mip_solver_settings_t { // TODO check with Akif and Alice pdlp_hyper_params::pdlp_hyper_params_t hyper_params; - mip_heuristics_hyper_params_t heuristic_params; + mip_heuristics_hyper_params_t heuristic_params; - mip_diving_hyper_params_t diving_params; + mip_diving_hyper_params_t diving_params; private: std::vector mip_callbacks_; diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 8283888442..a568aacd47 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -785,7 +785,7 @@ void branch_and_bound_t::add_feasible_solution(f_t leaf_objective, { bool send_solution = false; - const bool log_diving_type = settings_.diving_settings.show_diving_type; + const bool log_diving_type = settings_.diving_settings.show_type; settings_.log.debug("%c found a feasible solution with obj=%.10e.\n", feasible_solution_symbol(thread_type, log_diving_type), compute_user_objective(original_lp_, leaf_objective)); @@ -1740,7 +1740,7 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t settings_.bnb_steal_chance >= 0 ? settings_.bnb_steal_chance : MIP_DEFAULT_STEAL_CHANCE; node_queue_t& node_queue = worker->node_queue; - mip_diving_hyper_params_t diving_settings = settings_.diving_settings; + mip_diving_hyper_params_t diving_settings = settings_.diving_settings; if (diving_settings.guided_diving != 0 && !has_solver_space_incumbent()) { diving_settings.guided_diving = 0; } @@ -3528,7 +3528,7 @@ void branch_and_bound_t::deterministic_process_worker_solutions( i_t nodes_unexplored = exploration_stats_.nodes_unexplored.load(); search_strategy_t worker_type = get_worker_type(pool, sol->worker_id); - report(feasible_solution_symbol(worker_type, settings_.diving_settings.show_diving_type), + report(feasible_solution_symbol(worker_type, settings_.diving_settings.show_type), sol->objective, deterministic_lower, sol->depth, diff --git a/cpp/src/branch_and_bound/diving_heuristics.hpp b/cpp/src/branch_and_bound/diving_heuristics.hpp index fe7b296dff..f22434b5cd 100644 --- a/cpp/src/branch_and_bound/diving_heuristics.hpp +++ b/cpp/src/branch_and_bound/diving_heuristics.hpp @@ -7,6 +7,8 @@ #pragma once +#include + #include #include @@ -39,8 +41,9 @@ inline char feasible_solution_symbol(search_strategy_t strategy, bool log_diving } } -inline bool is_search_strategy_enabled(search_strategy_t strategy, - const mip_diving_hyper_params_t& settings) +template +bool is_search_strategy_enabled(search_strategy_t strategy, + const mip_diving_hyper_params_t& settings) { switch (strategy) { case BEST_FIRST: return true; diff --git a/cpp/src/branch_and_bound/worker.hpp b/cpp/src/branch_and_bound/worker.hpp index 0e0761e5e6..62bd0cde6b 100644 --- a/cpp/src/branch_and_bound/worker.hpp +++ b/cpp/src/branch_and_bound/worker.hpp @@ -176,7 +176,7 @@ class bfs_worker_t : public branch_and_bound_worker_t { // of workers allows the solver to be more deterministic. void calculate_num_diving_workers(i_t num_bfs_workers, i_t total_diving_workers, - const mip_diving_hyper_params_t& settings) + const mip_diving_hyper_params_t& settings) { i_t num_active = 0; for (i_t i = 1; i < num_search_strategies; ++i) { diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 3bbd85daf3..500af4286e 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -193,7 +193,7 @@ struct simplex_solver_settings_t { // >1 - set as the iteration limit in dual simplex i_t strong_branching_simplex_iteration_limit; - mip_diving_hyper_params_t diving_settings; // Settings for the diving heuristics + mip_diving_hyper_params_t diving_settings; // Settings for the diving heuristics // In B&B, indicate the chance in which a worker can steal a node from another worker. // -1 - automatic (0.05) diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 30e04fe3a9..df51dd688e 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -190,7 +190,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_BARRIER_ITERATIVE_REFINEMENT, &pdlp_settings.barrier_iterative_refinement, true}, {CUOPT_MIP_PROBING, &mip_settings.probing, true}, // Diving heuristic hyper-parameters (hidden from default --help: name contains "hyper_") - {CUOPT_MIP_HYPER_DIVING_SHOW_DIVING_TYPE, &mip_settings.diving_params.show_diving_type, false, "log diving heuristic type when it finds a new incumbent"}, + {CUOPT_MIP_HYPER_DIVING_SHOW_TYPE, &mip_settings.diving_params.show_type, false, "log diving heuristic type when it finds a new incumbent"}, }; // String parameters string_parameters = { diff --git a/cpp/tests/mip/heuristics_hyper_params_test.cu b/cpp/tests/mip/heuristics_hyper_params_test.cu index 50e463b1fe..24b15f8cc8 100644 --- a/cpp/tests/mip/heuristics_hyper_params_test.cu +++ b/cpp/tests/mip/heuristics_hyper_params_test.cu @@ -114,7 +114,7 @@ TEST_F(HeuristicsHyperParamsTest, PartialConfigKeepsDefaults) EXPECT_EQ(hp.population_size, 128); EXPECT_DOUBLE_EQ(hp.rins_fix_rate, 0.3); - mip_heuristics_hyper_params_t defaults; + mip_heuristics_hyper_params_t defaults; EXPECT_EQ(hp.num_cpufj_threads, defaults.num_cpufj_threads); EXPECT_DOUBLE_EQ(hp.presolve_time_ratio, defaults.presolve_time_ratio); EXPECT_EQ(hp.n_of_minimums_for_exit, defaults.n_of_minimums_for_exit); From c8a8bf499649219738649fc75c1dbbf5d83284f6 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Tue, 2 Jun 2026 14:19:12 +0200 Subject: [PATCH 3/4] implemented farkas and vector length diving. Signed-off-by: Nicolas L. Guidotti --- .../cuopt/linear_programming/constants.h | 10 +- .../mip/diving_hyper_params.hpp | 15 ++- cpp/src/branch_and_bound/branch_and_bound.cpp | 11 ++ cpp/src/branch_and_bound/constants.hpp | 26 ++-- .../branch_and_bound/diving_heuristics.cpp | 118 ++++++++++++++++++ .../branch_and_bound/diving_heuristics.hpp | 21 +++- cpp/src/dual_simplex/presolve.hpp | 5 + cpp/src/math_optimization/solver_settings.cu | 2 + 8 files changed, 191 insertions(+), 17 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index ae02ee00c7..024fae7583 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -115,10 +115,12 @@ "mip_hyper_heuristic_related_vars_time_limit" /* @brief Diving heuristic toggles: -1 automatic, 0 disabled, 1 enabled */ -#define CUOPT_MIP_HYPER_DIVING_LINE_SEARCH "mip_hyper_diving_line_search" -#define CUOPT_MIP_HYPER_DIVING_PSEUDOCOST "mip_hyper_diving_pseudocost" -#define CUOPT_MIP_HYPER_DIVING_GUIDED "mip_hyper_diving_guided" -#define CUOPT_MIP_HYPER_DIVING_COEFFICIENT "mip_hyper_diving_coefficient" +#define CUOPT_MIP_HYPER_DIVING_LINE_SEARCH "mip_hyper_diving_line_search" +#define CUOPT_MIP_HYPER_DIVING_PSEUDOCOST "mip_hyper_diving_pseudocost" +#define CUOPT_MIP_HYPER_DIVING_GUIDED "mip_hyper_diving_guided" +#define CUOPT_MIP_HYPER_DIVING_COEFFICIENT "mip_hyper_diving_coefficient" +#define CUOPT_MIP_HYPER_DIVING_FARKAS "mip_hyper_diving_farkas" +#define CUOPT_MIP_HYPER_DIVING_VECTOR_LENGTH "mip_hyper_diving_vector_length" /* @brief Diving heuristic limits */ #define CUOPT_MIP_HYPER_DIVING_MIN_NODE_DEPTH "mip_hyper_diving_min_node_depth" #define CUOPT_MIP_HYPER_DIVING_NODE_LIMIT "mip_hyper_diving_node_limit" diff --git a/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp b/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp index bd9e316104..19463b9150 100644 --- a/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp +++ b/cpp/include/cuopt/linear_programming/mip/diving_hyper_params.hpp @@ -20,10 +20,12 @@ namespace cuopt::linear_programming { template struct mip_diving_hyper_params_t { // -1 automatic, 0 disabled, 1 enabled - int line_search_diving = -1; - int pseudocost_diving = -1; - int guided_diving = -1; - int coefficient_diving = -1; + i_t line_search_diving = -1; + i_t pseudocost_diving = -1; + i_t guided_diving = -1; + i_t coefficient_diving = -1; + i_t farkas_diving = -1; + i_t vector_length_diving = -1; // The minimum depth to start diving from. i_t min_node_depth = 10; @@ -39,6 +41,11 @@ struct mip_diving_hyper_params_t { // The maximum backtracking allowed. i_t backtrack_limit = 5; + // For the Farkas diving to be effective, the coefficients in the objective function + // must have distinct values. The low tolerance here disables Farkas diving for + // set covering/partitioning, where all coefficients have the same value. + f_t farkas_obj_dynamism_tol = 1E-4; + // If a given diving heuristic found a new incumbent, show the corresponding // symbol in the first column of the log row. When false, every dive collapses // to 'D'. Otherwise, diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index a568aacd47..adb7154efc 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -882,6 +882,12 @@ branch_variable_t branch_and_bound_t::variable_selection( mutex_upper_.unlock(); return guided_diving(pc_, fractional, solution, current_incumbent, log); + case FARKAS_DIVING: + return farkas_diving(worker->leaf_problem, fractional, solution, settings_.zero_tol, log); + + case VECTOR_LENGTH_DIVING: + return vector_length_diving(worker->leaf_problem, fractional, solution, log); + default: log.debug("Unknown variable selection method: %d\n", worker->search_strategy); return {-1, branch_direction_t::NONE}; @@ -1745,6 +1751,11 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t diving_settings.guided_diving = 0; } + if (diving_settings.farkas_diving != 0) { + f_t obj_dyn = std::log10(original_lp_.max_abs_obj_coeff / original_lp_.min_abs_obj_coeff); + if (obj_dyn < diving_settings.farkas_obj_dynamism_tol) { diving_settings.farkas_diving = 0; } + } + worker->calculate_num_diving_workers( bfs_worker_pool_.size(), diving_worker_pool_.size(), diving_settings); diff --git a/cpp/src/branch_and_bound/constants.hpp b/cpp/src/branch_and_bound/constants.hpp index fed422ef89..d507999ed6 100644 --- a/cpp/src/branch_and_bound/constants.hpp +++ b/cpp/src/branch_and_bound/constants.hpp @@ -9,23 +9,33 @@ namespace cuopt::linear_programming::dual_simplex { -constexpr int num_search_strategies = 5; +constexpr int num_search_strategies = 7; // Indicate the search and variable selection algorithms used by each thread // in B&B (See [1]). // // [1] T. Achterberg, “Constraint Integer Programming,” PhD, Technischen Universität Berlin, // Berlin, 2007. doi: 10.14279/depositonce-1634. +// [2] J. Witzig and A. Gleixner, “Conflict-Driven Heuristics for Mixed Integer Programming,” +// Feb. 07, 2019, _arXiv_: arXiv:1902.02615. doi: +// [10.48550/arXiv.1902.02615](https://doi.org/10.48550/arXiv.1902.02615). enum search_strategy_t : int { - BEST_FIRST = 0, // Best-First + Plunging. - PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) - LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) - GUIDED_DIVING = 3, // Guided diving (9.2.3). - COEFFICIENT_DIVING = 4 // Coefficient diving (9.2.1) + BEST_FIRST = 0, // Best-First + Plunging. + PSEUDOCOST_DIVING = 1, // Pseudocost diving (9.2.5) + LINE_SEARCH_DIVING = 2, // Line search diving (9.2.4) + GUIDED_DIVING = 3, // Guided diving (9.2.3). + COEFFICIENT_DIVING = 4, // Coefficient diving (9.2.1) + FARKAS_DIVING = 5, // Farkas Diving (see [2]) + VECTOR_LENGTH_DIVING = 6 // Vector Length Diving (9.2.6) }; -constexpr search_strategy_t search_strategies[] = { - BEST_FIRST, PSEUDOCOST_DIVING, LINE_SEARCH_DIVING, GUIDED_DIVING, COEFFICIENT_DIVING}; +constexpr search_strategy_t search_strategies[] = {BEST_FIRST, + PSEUDOCOST_DIVING, + LINE_SEARCH_DIVING, + GUIDED_DIVING, + COEFFICIENT_DIVING, + FARKAS_DIVING, + VECTOR_LENGTH_DIVING}; enum class branch_direction_t { NONE = -1, DOWN = 0, UP = 1 }; diff --git a/cpp/src/branch_and_bound/diving_heuristics.cpp b/cpp/src/branch_and_bound/diving_heuristics.cpp index 46a6816795..933f67f506 100644 --- a/cpp/src/branch_and_bound/diving_heuristics.cpp +++ b/cpp/src/branch_and_bound/diving_heuristics.cpp @@ -259,6 +259,112 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl return {branch_var, round_dir}; } +template +branch_variable_t farkas_diving(const lp_problem_t& lp, + const std::vector& fractional, + const std::vector& solution, + f_t zero_tol, + logger_t& log) +{ + if (fractional.size() == 0) return {-1, branch_direction_t::NONE}; + + i_t branch_var = -1; + f_t max_score = -1; + branch_direction_t round_dir = branch_direction_t::NONE; + + for (i_t j : fractional) { + f_t c = lp.objective[j]; + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + f_t score = 0; + branch_direction_t dir = branch_direction_t::NONE; + + if (c > zero_tol) { + dir = branch_direction_t::DOWN; + } else if (c < -zero_tol) { + dir = branch_direction_t::UP; + } else if (f_down < 0.5) { + dir = branch_direction_t::DOWN; + } else { + dir = branch_direction_t::UP; + } + + if (dir == branch_direction_t::UP) { + score = std::isfinite(lp.upper[j]) + ? (lp.upper[j] - std::floor(solution[j])) * f_up * std::abs(c) + : std::numeric_limits::infinity(); + } else { + score = std::isfinite(lp.lower[j]) + ? (std::ceil(solution[j]) - lp.lower[j]) * f_down * std::abs(c) + : std::numeric_limits::infinity(); + } + + if (score > max_score) { + max_score = score; + branch_var = j; + round_dir = dir; + } + } + + assert(round_dir != branch_direction_t::NONE); + assert(branch_var >= 0); + + log.debug("Farkas diving: selected var %d with val = %e, round dir = %d\n", + branch_var, + solution[branch_var], + round_dir); + + return {branch_var, round_dir}; +} + +template +branch_variable_t vector_length_diving(const lp_problem_t& lp, + const std::vector& fractional, + const std::vector& solution, + logger_t& log) +{ + if (fractional.size() == 0) return {-1, branch_direction_t::NONE}; + + constexpr f_t eps = 1E-6; + i_t branch_var = -1; + f_t min_score = std::numeric_limits::infinity(); + branch_direction_t round_dir = branch_direction_t::NONE; + + for (i_t j : fractional) { + f_t c = lp.objective[j]; + f_t f_down = solution[j] - std::floor(solution[j]); + f_t f_up = std::ceil(solution[j]) - solution[j]; + i_t column_length = lp.A.col_start[j + 1] - lp.A.col_start[j]; + f_t score = 0; + branch_direction_t dir = branch_direction_t::NONE; + + if (c < 0) { + dir = branch_direction_t::DOWN; + score = (f_down * std::abs(c) + eps) / (column_length + 1); + } else { + dir = branch_direction_t::UP; + score = (f_up * std::abs(c) + eps) / (column_length + 1); + } + + if (score < min_score) { + branch_var = j; + round_dir = dir; + min_score = score; + } + } + + assert(round_dir != branch_direction_t::NONE); + assert(branch_var >= 0); + + log.debug("Vector length diving: selected var %d with val = %e, round dir = %d and score = %e\n", + branch_var, + solution[branch_var], + round_dir, + min_score); + + return {branch_var, round_dir}; +} + #ifdef DUAL_SIMPLEX_INSTANTIATE_DOUBLE template branch_variable_t line_search_diving(const std::vector& fractional, const std::vector& solution, @@ -287,6 +393,18 @@ template branch_variable_t coefficient_diving(const lp_problem_t& up_locks, const std::vector& down_locks, logger_t& log); + +template branch_variable_t farkas_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + double zero_tol, + logger_t& log); + +template branch_variable_t vector_length_diving(const lp_problem_t& lp_problem, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); + #endif } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/branch_and_bound/diving_heuristics.hpp b/cpp/src/branch_and_bound/diving_heuristics.hpp index f22434b5cd..003da75ea2 100644 --- a/cpp/src/branch_and_bound/diving_heuristics.hpp +++ b/cpp/src/branch_and_bound/diving_heuristics.hpp @@ -28,7 +28,9 @@ inline char feasible_solution_symbol(search_strategy_t strategy, bool log_diving case COEFFICIENT_DIVING: case LINE_SEARCH_DIVING: case PSEUDOCOST_DIVING: - case GUIDED_DIVING: return 'D'; + case GUIDED_DIVING: + case FARKAS_DIVING: + case VECTOR_LENGTH_DIVING: return 'D'; default: return 'U'; } } @@ -37,6 +39,8 @@ inline char feasible_solution_symbol(search_strategy_t strategy, bool log_diving case LINE_SEARCH_DIVING: return 'L'; case PSEUDOCOST_DIVING: return 'P'; case GUIDED_DIVING: return 'G'; + case FARKAS_DIVING: return 'F'; + case VECTOR_LENGTH_DIVING: return 'V'; default: return 'U'; } } @@ -51,6 +55,8 @@ bool is_search_strategy_enabled(search_strategy_t strategy, case LINE_SEARCH_DIVING: return settings.line_search_diving != 0; case GUIDED_DIVING: return settings.guided_diving != 0; case COEFFICIENT_DIVING: return settings.coefficient_diving != 0; + case FARKAS_DIVING: return settings.farkas_diving != 0; + case VECTOR_LENGTH_DIVING: return settings.vector_length_diving != 0; } return false; @@ -91,4 +97,17 @@ branch_variable_t coefficient_diving(const lp_problem_t& lp_probl const std::vector& down_locks, logger_t& log); +template +branch_variable_t farkas_diving(const lp_problem_t& lp, + const std::vector& fractional, + const std::vector& solution, + f_t zero_tol, + logger_t& log); + +template +branch_variable_t vector_length_diving(const lp_problem_t& lp, + const std::vector& fractional, + const std::vector& solution, + logger_t& log); + } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/presolve.hpp b/cpp/src/dual_simplex/presolve.hpp index c6587c9d79..22e578047f 100644 --- a/cpp/src/dual_simplex/presolve.hpp +++ b/cpp/src/dual_simplex/presolve.hpp @@ -53,6 +53,11 @@ struct lp_problem_t { i_t cone_var_start{0}; std::vector second_order_cone_dims; + // Maximum and minimum value of the coefficients in the objective function. This is used + // for determine the "objective dynamism" in Farkas diving. + f_t max_abs_obj_coeff = 0; + f_t min_abs_obj_coeff = 0; + void write_mps(const std::string& path) const { std::ofstream mps_file(path); diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index df51dd688e..0f5ffdb748 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -168,6 +168,8 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_DIVING_PSEUDOCOST, &mip_settings.diving_params.pseudocost_diving, -1, 1, -1, "pseudocost diving toggle: -1 automatic, 0 disabled, 1 enabled"}, {CUOPT_MIP_HYPER_DIVING_GUIDED, &mip_settings.diving_params.guided_diving, -1, 1, -1, "guided diving toggle: -1 automatic, 0 disabled, 1 enabled"}, {CUOPT_MIP_HYPER_DIVING_COEFFICIENT, &mip_settings.diving_params.coefficient_diving, -1, 1, -1, "coefficient diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_FARKAS, &mip_settings.diving_params.farkas_diving, -1, 1, -1, "Farkas diving toggle: -1 automatic, 0 disabled, 1 enabled"}, + {CUOPT_MIP_HYPER_DIVING_VECTOR_LENGTH, &mip_settings.diving_params.vector_length_diving, -1, 1, -1, "vector-length diving toggle: -1 automatic, 0 disabled, 1 enabled"}, {CUOPT_MIP_HYPER_DIVING_MIN_NODE_DEPTH, &mip_settings.diving_params.min_node_depth, 0, std::numeric_limits::max(), 10, "minimum depth at which to start diving"}, {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip_settings.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, From 78707b02e34780cec20a396cce0ff3c304b13549 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Mon, 8 Jun 2026 10:26:53 +0200 Subject: [PATCH 4/4] address coderabbit Signed-off-by: Nicolas L. Guidotti --- cpp/src/branch_and_bound/branch_and_bound.cpp | 16 +++++++++++++++- cpp/src/math_optimization/solver_settings.cu | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index adb7154efc..ac1a770b26 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1154,6 +1154,13 @@ struct deterministic_diving_policy_t log); } + case VECTOR_LENGTH_DIVING: + return vector_length_diving(this->worker.leaf_problem, fractional, x, log); + + case FARKAS_DIVING: + return farkas_diving( + this->worker.leaf_problem, fractional, x, this->bnb.settings_.zero_tol, log); + default: CUOPT_LOG_ERROR("Invalid diving method!"); return {-1, branch_direction_t::NONE}; } } @@ -1752,7 +1759,14 @@ void branch_and_bound_t::best_first_search_with(bfs_worker_t } if (diving_settings.farkas_diving != 0) { - f_t obj_dyn = std::log10(original_lp_.max_abs_obj_coeff / original_lp_.min_abs_obj_coeff); + f_t obj_dyn; + if (std::abs(original_lp_.min_abs_obj_coeff) < settings_.zero_tol) { + obj_dyn = std::abs(original_lp_.max_abs_obj_coeff) < settings_.zero_tol + ? 0 + : std::numeric_limits::infinity(); + } else { + obj_dyn = std::log10(original_lp_.max_abs_obj_coeff / original_lp_.min_abs_obj_coeff); + } if (obj_dyn < diving_settings.farkas_obj_dynamism_tol) { diving_settings.farkas_diving = 0; } } diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 0f5ffdb748..4a000c6e05 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -172,7 +172,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_HYPER_DIVING_VECTOR_LENGTH, &mip_settings.diving_params.vector_length_diving, -1, 1, -1, "vector-length diving toggle: -1 automatic, 0 disabled, 1 enabled"}, {CUOPT_MIP_HYPER_DIVING_MIN_NODE_DEPTH, &mip_settings.diving_params.min_node_depth, 0, std::numeric_limits::max(), 10, "minimum depth at which to start diving"}, {CUOPT_MIP_HYPER_DIVING_NODE_LIMIT, &mip_settings.diving_params.node_limit, 0, std::numeric_limits::max(), 500, "maximum nodes explored per dive"}, - {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, + {CUOPT_MIP_HYPER_DIVING_BACKTRACK_LIMIT, &mip_settings.diving_params.backtrack_limit, 0, std::numeric_limits::max(), 5, "maximum backtracking allowed per dive"}, }; // Bool parameters