Skip to content

Commit d862480

Browse files
authored
Add clique cuts (#937)
This PR adds clique cuts to the set of cuts we have. It uses the ideas from Preprocessing and Cutting Planes with Conflict Graphs, Brito et.al. In a previous PR, I have added the clique table generation as a preprocessing step (currently doesn't change the problem). This PR builds on that by using the extended cliques during cut passes. At each cut pass, a subgraph induced by the fractinoal variables on the current relaxation solution is used to generate clique cuts. We want to generate the set of violated clique inequalities that have at least min violation (1e-6). The maximal cliques in the fractional subgraph is found by Bron Kerbosh dynamic programming algorithm. The found maximal cliques are extended with the suitable integer valued variables on the original conflict graph. The extension is guided by reduced cost of the variables at the current relaxation optimal. We try to extend the cliques with variables that have lower reduced cost. The reason is that it disturbs dual simplex less (causes fewer refactors and iterations for resolve). Also the variables with lower reduced costs are likely to be active in the next resolve. This also results in more dominant cliques preventing multiple smaller sized cliques in the next iterations. Benchmark results: MIP GAP wins/losses defined as at least 10% difference: main wins (6): Instance dir1 gap dir2 gap diff app1-2 0.8077 1.1818 +0.3741 bab2 0.4148 0.6148 +0.2001 glass4 0.3333 0.4483 +0.1149 map16715-04 0.9269 1.4085 +0.4816 neos-4413714-turia 0.7027 0.8454 +0.1427 rail01 0.5476 0.7934 +0.2458 clique cuts wins (11): Instance dir1 gap dir2 gap diff 30n20b8 0.7688 0.6667 +0.1021 bab6 0.9255 0.3611 +0.5643 buildingenergy 0.1706 0.0036 +0.1670 co-100 0.8410 0.7089 +0.1321 neos-3754480-nidda 20.3512 19.6495 +0.7017 neos-5188808-nattai 0.2032 0.0000 +0.2032 netdiversion 0.3710 0.1429 +0.2281 physiciansched3-3 0.2444 0.0269 +0.2175 rocII-5-11 1.0893 0.7762 +0.3131 satellites2-60-fs 13.5000 1.4167 +12.0833 sorrell3 9.5625 9.4375 +0.1250 Optimality wins/loses: Wins graphdraw-domain (443.68s) neos-5188808-nattai (462.84s) supportcase26 (537.66s) Losses dano3_3 (121.30s) Time to optimality geomean (shifted geomean by 1.0): on 50 common optimal instances Common optimal instances: 50 Geometric mean of ratios (main/clique): 1.1925 Authors: - Akif ÇÖRDÜK (https://github.com/akifcorduk) - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Chris Maes (https://github.com/chris-maes) URL: #937
1 parent 2b21118 commit d862480

22 files changed

Lines changed: 2649 additions & 360 deletions

File tree

benchmarks/linear_programming/cuopt/run_mip.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ int run_single_file(std::string file_path,
183183
CUOPT_LOG_ERROR("Parsing MPS failed exiting!");
184184
return -1;
185185
}
186+
// Use the benchmark filename for downstream instance-level reporting.
187+
// This keeps per-instance metrics aligned with the run list even if the MPS NAME card differs.
188+
mps_data_model.set_problem_name(base_filename);
189+
186190
if (initial_solution_dir.has_value()) {
187191
auto initial_solutions = read_solution_from_dir(
188192
initial_solution_dir.value(), base_filename, mps_data_model.get_variable_names());
@@ -209,6 +213,7 @@ int run_single_file(std::string file_path,
209213
settings.tolerances.absolute_tolerance = 1e-6;
210214
settings.presolver = cuopt::linear_programming::presolver_t::Default;
211215
settings.reliability_branching = reliability_branching;
216+
settings.clique_cuts = -1;
212217
settings.seed = 42;
213218
cuopt::linear_programming::benchmark_info_t benchmark_info;
214219
settings.benchmark_info_ptr = &benchmark_info;
@@ -413,7 +418,16 @@ int main(int argc, char* argv[])
413418
int reliability_branching = program.get<int>("--reliability-branching");
414419
bool deterministic = program.get<bool>("--determinism");
415420

416-
if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; }
421+
if (num_cpu_threads < 0) {
422+
num_cpu_threads = omp_get_max_threads() / n_gpus;
423+
// std::ifstream smt_file("/sys/devices/system/cpu/smt/active");
424+
// if (smt_file.is_open()) {
425+
// int smt_active = 0;
426+
// smt_file >> smt_active;
427+
// if (smt_active) { num_cpu_threads /= 2; }
428+
// }
429+
num_cpu_threads = std::max(num_cpu_threads, 1);
430+
}
417431

418432
if (program.is_used("--out-dir")) {
419433
out_dir = program.get<std::string>("--out-dir");

cpp/include/cuopt/linear_programming/constants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
#define CUOPT_MIP_MIXED_INTEGER_ROUNDING_CUTS "mip_mixed_integer_rounding_cuts"
6565
#define CUOPT_MIP_MIXED_INTEGER_GOMORY_CUTS "mip_mixed_integer_gomory_cuts"
6666
#define CUOPT_MIP_KNAPSACK_CUTS "mip_knapsack_cuts"
67+
#define CUOPT_MIP_CLIQUE_CUTS "mip_clique_cuts"
6768
#define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts"
6869
#define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening"
6970
#define CUOPT_MIP_CUT_CHANGE_THRESHOLD "mip_cut_change_threshold"

cpp/include/cuopt/linear_programming/mip/solver_settings.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class mip_solver_settings_t {
9393
i_t mir_cuts = -1;
9494
i_t mixed_integer_gomory_cuts = -1;
9595
i_t knapsack_cuts = -1;
96+
i_t clique_cuts = -1;
9697
i_t strong_chvatal_gomory_cuts = -1;
9798
i_t reduced_cost_strengthening = -1;
9899
f_t cut_change_threshold = 1e-3;

cpp/src/branch_and_bound/branch_and_bound.cpp

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <branch_and_bound/pseudo_costs.hpp>
1111

1212
#include <cuts/cuts.hpp>
13+
#include <mip_heuristics/presolve/conflict_graph/clique_table.cuh>
1314

1415
#include <dual_simplex/basis_solves.hpp>
1516
#include <dual_simplex/bounds_strengthening.hpp>
@@ -241,9 +242,11 @@ template <typename i_t, typename f_t>
241242
branch_and_bound_t<i_t, f_t>::branch_and_bound_t(
242243
const user_problem_t<i_t, f_t>& user_problem,
243244
const simplex_solver_settings_t<i_t, f_t>& solver_settings,
244-
f_t start_time)
245+
f_t start_time,
246+
std::shared_ptr<detail::clique_table_t<i_t, f_t>> clique_table)
245247
: original_problem_(user_problem),
246248
settings_(solver_settings),
249+
clique_table_(std::move(clique_table)),
247250
original_lp_(user_problem.handle_ptr, 1, 1, 1),
248251
Arow_(1, 1, 0),
249252
incumbent_(1),
@@ -1967,6 +1970,31 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
19671970

19681971
root_relax_soln_.resize(original_lp_.num_rows, original_lp_.num_cols);
19691972

1973+
if (settings_.clique_cuts != 0 && clique_table_ == nullptr) {
1974+
signal_extend_cliques_.store(false, std::memory_order_release);
1975+
typename ::cuopt::linear_programming::mip_solver_settings_t<i_t, f_t>::tolerances_t
1976+
tolerances_for_clique{};
1977+
tolerances_for_clique.presolve_absolute_tolerance = settings_.primal_tol;
1978+
tolerances_for_clique.absolute_tolerance = settings_.primal_tol;
1979+
tolerances_for_clique.relative_tolerance = settings_.zero_tol;
1980+
tolerances_for_clique.integrality_tolerance = settings_.integer_tol;
1981+
tolerances_for_clique.absolute_mip_gap = settings_.absolute_mip_gap_tol;
1982+
tolerances_for_clique.relative_mip_gap = settings_.relative_mip_gap_tol;
1983+
auto* signal_ptr = &signal_extend_cliques_;
1984+
clique_table_future_ =
1985+
std::async(std::launch::async,
1986+
[this,
1987+
tolerances_for_clique,
1988+
signal_ptr]() -> std::shared_ptr<detail::clique_table_t<i_t, f_t>> {
1989+
user_problem_t<i_t, f_t> problem_copy = original_problem_;
1990+
cuopt::timer_t timer(std::numeric_limits<double>::infinity());
1991+
std::shared_ptr<detail::clique_table_t<i_t, f_t>> table;
1992+
detail::find_initial_cliques(
1993+
problem_copy, tolerances_for_clique, &table, timer, false, signal_ptr);
1994+
return table;
1995+
});
1996+
}
1997+
19701998
i_t original_rows = original_lp_.num_rows;
19711999
simplex_solver_settings_t lp_settings = settings_;
19722000
lp_settings.inside_mip = 1;
@@ -2002,38 +2030,44 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
20022030
exploration_stats_.total_lp_iters = root_relax_soln_.iterations;
20032031
exploration_stats_.total_lp_solve_time = toc(exploration_stats_.start_time);
20042032

2033+
auto finish_clique_thread = [this]() {
2034+
if (clique_table_future_.valid()) {
2035+
signal_extend_cliques_.store(true, std::memory_order_release);
2036+
clique_table_ = clique_table_future_.get();
2037+
}
2038+
};
2039+
20052040
if (root_status == lp_status_t::INFEASIBLE) {
20062041
settings_.log.printf("MIP Infeasible\n");
2007-
// FIXME: rarely dual simplex detects infeasible whereas it is feasible.
2008-
// to add a small safety net, check if there is a primal solution already.
2009-
// Uncomment this if the issue with cost266-UUE is resolved
2010-
// if (settings.heuristic_preemption_callback != nullptr) {
2011-
// settings.heuristic_preemption_callback();
2012-
// }
2042+
finish_clique_thread();
20132043
return mip_status_t::INFEASIBLE;
20142044
}
20152045
if (root_status == lp_status_t::UNBOUNDED) {
20162046
settings_.log.printf("MIP Unbounded\n");
20172047
if (settings_.heuristic_preemption_callback != nullptr) {
20182048
settings_.heuristic_preemption_callback();
20192049
}
2050+
finish_clique_thread();
20202051
return mip_status_t::UNBOUNDED;
20212052
}
20222053
if (root_status == lp_status_t::TIME_LIMIT) {
20232054
solver_status_ = mip_status_t::TIME_LIMIT;
20242055
set_final_solution(solution, -inf);
2056+
finish_clique_thread();
20252057
return solver_status_;
20262058
}
20272059

20282060
if (root_status == lp_status_t::WORK_LIMIT) {
20292061
solver_status_ = mip_status_t::WORK_LIMIT;
20302062
set_final_solution(solution, -inf);
2063+
finish_clique_thread();
20312064
return solver_status_;
20322065
}
20332066

20342067
if (root_status == lp_status_t::NUMERICAL_ISSUES) {
20352068
solver_status_ = mip_status_t::NUMERICAL;
20362069
set_final_solution(solution, -inf);
2070+
finish_clique_thread();
20372071
return solver_status_;
20382072
}
20392073

@@ -2064,6 +2098,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
20642098

20652099
if (num_fractional == 0) {
20662100
set_solution_at_root(solution, cut_info);
2101+
finish_clique_thread();
20672102
return mip_status_t::OPTIMAL;
20682103
}
20692104

@@ -2078,8 +2113,16 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
20782113
}
20792114

20802115
cut_pool_t<i_t, f_t> cut_pool(original_lp_.num_cols, settings_);
2081-
cut_generation_t<i_t, f_t> cut_generation(
2082-
cut_pool, original_lp_, settings_, Arow_, new_slacks_, var_types_);
2116+
cut_generation_t<i_t, f_t> cut_generation(cut_pool,
2117+
original_lp_,
2118+
settings_,
2119+
Arow_,
2120+
new_slacks_,
2121+
var_types_,
2122+
original_problem_,
2123+
clique_table_,
2124+
&clique_table_future_,
2125+
&signal_extend_cliques_);
20832126

20842127
std::vector<f_t> saved_solution;
20852128
#ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION
@@ -2090,7 +2133,8 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
20902133
f_t last_objective = root_objective_;
20912134
f_t root_relax_objective = root_objective_;
20922135

2093-
i_t cut_pool_size = 0;
2136+
f_t cut_generation_start_time = tic();
2137+
i_t cut_pool_size = 0;
20942138
for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) {
20952139
if (num_fractional == 0) {
20962140
set_solution_at_root(solution, cut_info);
@@ -2109,16 +2153,25 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
21092153
#endif
21102154

21112155
// Generate cuts and add them to the cut pool
2112-
f_t cut_start_time = tic();
2113-
cut_generation.generate_cuts(original_lp_,
2114-
settings_,
2115-
Arow_,
2116-
new_slacks_,
2117-
var_types_,
2118-
basis_update,
2119-
root_relax_soln_.x,
2120-
basic_list,
2121-
nonbasic_list);
2156+
f_t cut_start_time = tic();
2157+
bool problem_feasible = cut_generation.generate_cuts(original_lp_,
2158+
settings_,
2159+
Arow_,
2160+
new_slacks_,
2161+
var_types_,
2162+
basis_update,
2163+
root_relax_soln_.x,
2164+
root_relax_soln_.z,
2165+
basic_list,
2166+
nonbasic_list,
2167+
exploration_stats_.start_time);
2168+
if (!problem_feasible) {
2169+
if (settings_.heuristic_preemption_callback != nullptr) {
2170+
settings_.heuristic_preemption_callback();
2171+
}
2172+
finish_clique_thread();
2173+
return mip_status_t::INFEASIBLE;
2174+
}
21222175
f_t cut_generation_time = toc(cut_start_time);
21232176
if (cut_generation_time > 1.0) {
21242177
settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time);
@@ -2339,8 +2392,9 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
23392392
}
23402393

23412394
print_cut_info(settings_, cut_info);
2342-
2395+
f_t cut_generation_time = toc(cut_generation_start_time);
23432396
if (cut_info.has_cuts()) {
2397+
settings_.log.printf("Cut generation time: %.2f seconds\n", cut_generation_time);
23442398
settings_.log.printf("Cut pool size : %d\n", cut_pool_size);
23452399
settings_.log.printf("Size with cuts : %d constraints, %d variables, %d nonzeros\n",
23462400
original_lp_.num_rows,

cpp/src/branch_and_bound/branch_and_bound.hpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,17 @@
3232

3333
#include <omp.h>
3434

35+
#include <atomic>
3536
#include <functional>
37+
#include <future>
38+
#include <memory>
3639
#include <vector>
3740

41+
namespace cuopt::linear_programming::detail {
42+
template <typename i_t, typename f_t>
43+
struct clique_table_t;
44+
}
45+
3846
namespace cuopt::linear_programming::dual_simplex {
3947

4048
enum class mip_status_t {
@@ -68,7 +76,8 @@ class branch_and_bound_t {
6876
public:
6977
branch_and_bound_t(const user_problem_t<i_t, f_t>& user_problem,
7078
const simplex_solver_settings_t<i_t, f_t>& solver_settings,
71-
f_t start_time);
79+
f_t start_time,
80+
std::shared_ptr<detail::clique_table_t<i_t, f_t>> clique_table = nullptr);
7281

7382
// Set an initial guess based on the user_problem. This should be called before solve.
7483
void set_initial_guess(const std::vector<f_t>& user_guess) { guess_ = user_guess; }
@@ -106,8 +115,6 @@ class branch_and_bound_t {
106115

107116
void set_concurrent_lp_root_solve(bool enable) { enable_concurrent_lp_root_solve_ = enable; }
108117

109-
bool stop_for_time_limit(mip_solution_t<i_t, f_t>& solution);
110-
111118
// Repair a low-quality solution from the heuristics.
112119
bool repair_solution(const std::vector<f_t>& leaf_edge_norms,
113120
const std::vector<f_t>& potential_solution,
@@ -141,6 +148,9 @@ class branch_and_bound_t {
141148
private:
142149
const user_problem_t<i_t, f_t>& original_problem_;
143150
const simplex_solver_settings_t<i_t, f_t> settings_;
151+
std::shared_ptr<detail::clique_table_t<i_t, f_t>> clique_table_;
152+
std::future<std::shared_ptr<detail::clique_table_t<i_t, f_t>>> clique_table_future_;
153+
std::atomic<bool> signal_extend_cliques_{false};
144154

145155
work_limit_context_t work_unit_context_{"B&B"};
146156

0 commit comments

Comments
 (0)