Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cpp/include/cuopt/linear_programming/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_TYPE "mip_hyper_diving_show_type"

/* @brief MIP determinism mode constants */
#define CUOPT_MODE_OPPORTUNISTIC 0
#define CUOPT_MODE_DETERMINISTIC 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* 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().
*/
template <typename i_t, typename f_t>
struct mip_diving_hyper_params_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;

// 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_type = false;
};

} // namespace cuopt::linear_programming
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename i_t, typename f_t>
struct mip_heuristics_hyper_params_t {
int population_size = 32; // max solutions in pool
int num_cpufj_threads = 8; // parallel CPU FJ climbers
double presolve_time_ratio = 0.1; // fraction of total time for presolve
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <vector>

#include <cuopt/linear_programming/constants.h>
#include <cuopt/linear_programming/mip/diving_hyper_params.hpp>
#include <cuopt/linear_programming/mip/heuristics_hyper_params.hpp>
#include <cuopt/linear_programming/pdlp/pdlp_hyper_params.cuh>
#include <cuopt/linear_programming/utilities/internals.hpp>
Expand Down Expand Up @@ -162,7 +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<i_t, f_t> heuristic_params;

mip_diving_hyper_params_t<i_t, f_t> diving_params;

private:
std::vector<internals::base_solution_callback_t*> mip_callbacks_;
Expand Down
57 changes: 19 additions & 38 deletions cpp/src/branch_and_bound/branch_and_bound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,32 +213,6 @@ std::string user_mip_gap(const lp_problem_t<i_t, f_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 <typename i_t, typename f_t>
Expand Down Expand Up @@ -811,15 +785,20 @@ void branch_and_bound_t<i_t, f_t>::add_feasible_solution(f_t leaf_objective,
{
bool send_solution = false;

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),
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;
}

Expand Down Expand Up @@ -1761,22 +1740,24 @@ void branch_and_bound_t<i_t, f_t>::best_first_search_with(bfs_worker_t<i_t, f_t>
settings_.bnb_steal_chance >= 0 ? settings_.bnb_steal_chance : MIP_DEFAULT_STEAL_CHANCE;
node_queue_t<i_t, f_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<i_t, f_t> diving_settings = settings_.diving_settings;
if (diving_settings.guided_diving != 0 && !has_solver_space_incumbent()) {
diving_settings.guided_diving = 0;
Comment thread
nguidotti marked this conversation as resolved.
}

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);
Comment thread
nguidotti marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -3547,7 +3528,7 @@ void branch_and_bound_t<i_t, f_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_type),
sol->objective,
deterministic_lower,
sol->depth,
Expand Down
12 changes: 12 additions & 0 deletions cpp/src/branch_and_bound/diving_heuristics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ branch_variable_t<i_t> pseudocost_diving(pseudo_costs_t<i_t, f_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};
}

Expand Down Expand Up @@ -168,6 +174,12 @@ branch_variable_t<i_t> guided_diving(pseudo_costs_t<i_t, f_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};
}

Expand Down
32 changes: 32 additions & 0 deletions cpp/src/branch_and_bound/diving_heuristics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#pragma once

#include <cuopt/linear_programming/mip/diving_hyper_params.hpp>

#include <branch_and_bound/pseudo_costs.hpp>

#include <dual_simplex/basis_updates.hpp>
Expand All @@ -16,6 +18,36 @@

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) { return 'D'; }
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';
}
}

template <typename i_t, typename f_t>
bool is_search_strategy_enabled(search_strategy_t strategy,
const mip_diving_hyper_params_t<i_t, f_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 <typename i_t, typename f_t>
branch_variable_t<i_t> line_search_diving(const std::vector<i_t>& fractional,
const std::vector<f_t>& solution,
Expand Down
7 changes: 6 additions & 1 deletion cpp/src/branch_and_bound/pseudo_costs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

#include <branch_and_bound/constants.hpp>
#include <branch_and_bound/mip_node.hpp>
#include <branch_and_bound/worker.hpp>

#include <dual_simplex/basis_updates.hpp>
#include <dual_simplex/logger.hpp>
Expand All @@ -30,6 +29,12 @@ namespace cuopt::linear_programming::dual_simplex {
template <typename i_t, typename f_t>
struct mip_symmetry_t;

template <typename i_t, typename f_t>
struct branch_and_bound_worker_t;

template <typename i_t, typename f_t>
struct branch_and_bound_stats_t;

template <typename i_t, typename f_t>
struct reliability_branching_settings_t {
// Lower bound for the maximum number of LP iterations for a single trial branching
Expand Down
24 changes: 4 additions & 20 deletions cpp/src/branch_and_bound/worker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#pragma once

#include <branch_and_bound/constants.hpp>
#include <branch_and_bound/diving_heuristics.hpp>
#include <branch_and_bound/mip_node.hpp>
#include <branch_and_bound/node_queue.hpp>
#include <branch_and_bound/symmetry.hpp>
Expand Down Expand Up @@ -42,22 +43,6 @@ struct branch_and_bound_stats_t {
omp_atomic_t<int64_t> lexical_reduction_pruned_nodes = 0;
};

template <typename f_t, typename i_t>
bool is_search_strategy_enabled(search_strategy_t strategy,
bool has_incumbent,
diving_heuristics_settings_t<i_t, f_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 <typename i_t, typename f_t>
class branch_and_bound_worker_t {
public:
Expand Down Expand Up @@ -191,20 +176,19 @@ class bfs_worker_t : public branch_and_bound_worker_t<i_t, f_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<i_t, f_t>& settings)
const mip_diving_hyper_params_t<i_t, f_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;
max_diving_workers.fill(0);
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;
Expand Down
Loading