Skip to content

Commit ef55301

Browse files
authored
Fix adj_list check bug and improve clique cuts (#1386)
Improve clique cut generation by simplifing the conflict-graph clique table and fix complement-pair handling in clique cut construction drop the paired variable to form a valid fixing cut; detect contradictory variable/complement cliques as infeasible. Fixes a bug of skipping the small adjacency-list cliques, now we use clique_table_t::empty() function to check all clique data structures. The mip gap or gap glosed doesn't change significantly but we add on average 1.5 optimal solutions. Tested on 2 batches for main and this PR. ``` PR_1 -> total_instances: 240, total_feasible: 228, average_error_gap: 10.75, total_optimal: 77, avg_mip_gap: 0.3136, geomean_mip_gap: 0.1990, n_low_error: 129, average_gap_closed: 0.29 PR_2 -> total_instances: 240, total_feasible: 225, average_error_gap: 12.49, total_optimal: 78, avg_mip_gap: 0.3116, geomean_mip_gap: 0.1958, n_low_error: 126, average_gap_closed: 0.29 main_1 -> total_instances: 240, total_feasible: 224, average_error_gap: 13.29, total_optimal: 76, avg_mip_gap: 0.3054, geomean_mip_gap: 0.1938, n_low_error: 124, average_gap_closed: 0.29 main_2 -> total_instances: 240, total_feasible: 228, average_error_gap: 11.04, total_optimal: 76, avg_mip_gap: 0.3243, geomean_mip_gap: 0.2001, n_low_error: 124, average_gap_closed: 0.30 ``` Authors: - Akif ÇÖRDÜK (https://github.com/akifcorduk) Approvers: - Alice Boucher (https://github.com/aliceb-nv) URL: #1386
1 parent 8e3cfe7 commit ef55301

12 files changed

Lines changed: 963 additions & 627 deletions

File tree

benchmarks/linear_programming/cuopt/miplib2017_bks.hpp

Lines changed: 464 additions & 0 deletions
Large diffs are not rendered by default.

benchmarks/linear_programming/cuopt/run_mip.cpp

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/* clang-format on */
77
#include "initial_solution_reader.hpp"
88
#include "mip_test_instances.hpp"
9+
#include "miplib2017_bks.hpp"
910

1011
#include <cstdio>
1112
#include <cuopt/linear_programming/io/parser.hpp>
@@ -239,6 +240,43 @@ int run_single_file(std::string file_path,
239240
} else {
240241
CUOPT_LOG_INFO("%s: no solution found", base_filename.c_str());
241242
}
243+
244+
// Per-instance "gap closed to BKS" stat. Emits a single
245+
// grep-friendly "MIPLIBGapStat ..." line via printf so cross-branch
246+
// comparison is just `grep '^MIPLIBGapStat' branchA.log` then diff.
247+
// BKS values are looked up from the in-source MIPLIB2017 benchmark-set
248+
// table (miplib2017_bks.hpp); unknown instances emit "opt=TBD"
249+
// and infeasibility-flagged instances emit "opt=Infeasible".
250+
{
251+
const double _gap_seconds = std::chrono::duration_cast<std::chrono::milliseconds>(
252+
std::chrono::high_resolution_clock::now() - start_run_solver)
253+
.count() /
254+
1000.0;
255+
std::string _status_str;
256+
switch (solution.get_termination_status()) {
257+
case cuopt::linear_programming::mip_termination_status_t::Optimal:
258+
_status_str = "Optimal";
259+
break;
260+
case cuopt::linear_programming::mip_termination_status_t::FeasibleFound:
261+
_status_str = "FeasibleFound";
262+
break;
263+
case cuopt::linear_programming::mip_termination_status_t::TimeLimit:
264+
_status_str = "TimeLimit";
265+
break;
266+
case cuopt::linear_programming::mip_termination_status_t::Infeasible:
267+
_status_str = "Infeasible";
268+
break;
269+
default: _status_str = "Other"; break;
270+
}
271+
cuopt_bench::print_miplib_gap_stat(base_filename,
272+
solution,
273+
_gap_seconds,
274+
_status_str,
275+
benchmark_info.root_lp_no_cuts,
276+
benchmark_info.root_lp_with_cuts,
277+
benchmark_info.cut_generation_time_sec);
278+
}
279+
242280
std::stringstream ss;
243281
int decimal_places = 2;
244282
double mip_gap = solution.get_mip_gap();
@@ -534,31 +572,36 @@ int main(int argc, char* argv[])
534572
merge_result_files(out_dir, result_file, n_gpus, batch_num);
535573
} else {
536574
auto memory_resource = make_async();
575+
auto run_single = [&]() {
576+
run_single_file(path,
577+
0,
578+
0,
579+
n_gpus,
580+
out_dir,
581+
initial_solution_file,
582+
heuristics_only,
583+
num_cpu_threads,
584+
write_log_file,
585+
log_to_console,
586+
reliability_branching,
587+
time_limit,
588+
work_limit,
589+
deterministic);
590+
};
537591
if (memory_limit > 0) {
538592
auto limiting_adaptor =
539593
rmm::mr::limiting_resource_adaptor(memory_resource, memory_limit * 1024ULL * 1024ULL);
540594
rmm::mr::set_current_device_resource(limiting_adaptor);
595+
run_single();
541596
} else if (track_allocations) {
542597
rmm::mr::tracking_resource_adaptor tracking_adaptor(memory_resource,
543598
/*capture_stacks=*/true);
544599
rmm::mr::set_current_device_resource(tracking_adaptor);
600+
run_single();
545601
} else {
546602
rmm::mr::set_current_device_resource(memory_resource);
603+
run_single();
547604
}
548-
run_single_file(path,
549-
0,
550-
0,
551-
n_gpus,
552-
out_dir,
553-
initial_solution_file,
554-
heuristics_only,
555-
num_cpu_threads,
556-
write_log_file,
557-
log_to_console,
558-
reliability_branching,
559-
time_limit,
560-
work_limit,
561-
deterministic);
562605
}
563606

564607
return 0;

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,27 @@ struct benchmark_info_t {
2727
double last_improvement_of_best_feasible = 0;
2828
double last_improvement_after_recombination = 0;
2929
double objective_of_initial_population = std::numeric_limits<double>::max();
30+
// LP relaxation objective at the root node, BEFORE any cuts have been
31+
// added. quiet_NaN() means "B&B did not run cut passes / value was
32+
// never written" — distinguishes it from a legitimate 0.0.
33+
double root_lp_no_cuts = std::numeric_limits<double>::quiet_NaN();
34+
// LP relaxation objective at the root node, AFTER the full cut loop
35+
// (final pass result). The dual gap "by cuts at the root" is then
36+
// gap_after_cuts = opt - root_lp_with_cuts (in B&B's solver
37+
// objective sense)
38+
// and the classical "gap closed by cuts" metric is
39+
// gap_closed_pct = 100 * (root_lp_with_cuts - root_lp_no_cuts)
40+
// / (opt - root_lp_no_cuts).
41+
// quiet_NaN() means "B&B did not finish the cut loop / value not written".
42+
double root_lp_with_cuts = std::numeric_limits<double>::quiet_NaN();
43+
44+
// Wall-clock time spent inside the root-node cut generation loop
45+
// (sum of generate_cuts + score_cuts + check_for_duplicate_cuts +
46+
// get_best_cuts + add_cuts + post-cut LP resolves), in seconds.
47+
// Published by branch_and_bound.cpp::solve() at the same point that
48+
// root_lp_with_cuts is finalised. quiet_NaN() means "cut loop did
49+
// not run / value never written".
50+
double cut_generation_time_sec = std::numeric_limits<double>::quiet_NaN();
3051
};
3152

3253
// Forward declare solver_settings_t for friend class

cpp/src/branch_and_bound/branch_and_bound.cpp

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <branch_and_bound/pseudo_costs.hpp>
1212
#include <branch_and_bound/symmetry.hpp>
1313

14+
#include <cuopt/linear_programming/mip/solver_settings.hpp> // benchmark_info_t
15+
1416
#include <cuts/cuts.hpp>
1517
#include <mip_heuristics/feasibility_jump/cpu_fj_thread.cuh>
1618
#include <mip_heuristics/mip_constants.hpp>
@@ -2342,6 +2344,11 @@ auto branch_and_bound_t<i_t, f_t>::do_cut_pass(
23422344
}
23432345
root_objective_ = compute_objective(original_lp_, root_relax_soln_.x);
23442346

2347+
if (settings_.benchmark_info_ptr != nullptr) {
2348+
settings_.benchmark_info_ptr->root_lp_with_cuts =
2349+
compute_user_objective(original_lp_, root_objective_);
2350+
}
2351+
23452352
f_t remove_cuts_start_time = tic();
23462353
mutex_original_lp_.lock();
23472354
remove_cuts(original_lp_,
@@ -2460,7 +2467,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
24602467
user_problem_t<i_t, f_t> problem_copy = original_problem_;
24612468
timer_t timer(std::numeric_limits<double>::infinity());
24622469
detail::find_initial_cliques(
2463-
problem_copy, tolerances_for_clique, &clique_table_, timer, false, clique_signal);
2470+
problem_copy, tolerances_for_clique, &clique_table_, timer, clique_signal);
24642471
}
24652472
}
24662473

@@ -2569,6 +2576,11 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
25692576
cut_info_t<i_t, f_t> cut_info;
25702577

25712578
if (num_fractional == 0) {
2579+
if (settings_.benchmark_info_ptr != nullptr) {
2580+
const double v = static_cast<double>(compute_user_objective(original_lp_, root_objective_));
2581+
settings_.benchmark_info_ptr->root_lp_no_cuts = v;
2582+
settings_.benchmark_info_ptr->root_lp_with_cuts = v;
2583+
}
25722584
set_solution_at_root(solution, cut_info);
25732585
signal_extend_cliques_.store(true, std::memory_order_release);
25742586
#pragma omp taskwait depend(in : *clique_signal)
@@ -2605,6 +2617,15 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
26052617
f_t last_objective = root_objective_;
26062618
f_t root_relax_objective = root_objective_;
26072619

2620+
// Publish the no-cuts root LP value once. The with-cuts companion is
2621+
// published below after the cut loop terminates. Both go to the
2622+
// benchmark_info_t so callers (run_mip.cpp) can compute
2623+
// gap-closed-by-cuts without instrumenting the cut loop directly.
2624+
if (settings_.benchmark_info_ptr != nullptr) {
2625+
settings_.benchmark_info_ptr->root_lp_no_cuts =
2626+
compute_user_objective(original_lp_, root_relax_objective);
2627+
}
2628+
26082629
constexpr bool enable_root_cut_cpufj = true;
26092630
std::unique_ptr<detail::fj_cpu_task_t<i_t, f_t>> root_cut_cpufj_task;
26102631
auto root_cut_cpufj_improvement_callback =
@@ -2633,7 +2654,17 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
26332654
i_t cut_pool_size = 0;
26342655
for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) {
26352656
if (num_fractional == 0) {
2657+
// LP relaxation is already integer-feasible — solved at the root
2658+
// by the cuts added so far (possibly zero). Publish the with-cuts
2659+
// value so the gap-closed line still has a non-NaN dual bound.
2660+
if (settings_.benchmark_info_ptr != nullptr) {
2661+
settings_.benchmark_info_ptr->root_lp_with_cuts =
2662+
compute_user_objective(original_lp_, root_objective_);
2663+
}
26362664
set_solution_at_root(solution, cut_info);
2665+
if (settings_.benchmark_info_ptr != nullptr) {
2666+
settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time);
2667+
}
26372668
signal_extend_cliques_.store(true, std::memory_order_release);
26382669
#pragma omp taskwait depend(in : *clique_signal)
26392670
return mip_status_t::OPTIMAL;
@@ -2673,6 +2704,9 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
26732704
}
26742705

26752706
if (cut_pass_result.action == cut_pass_action_t::RETURN) {
2707+
if (settings_.benchmark_info_ptr != nullptr) {
2708+
settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time);
2709+
}
26762710
signal_extend_cliques_.store(true, std::memory_order_release);
26772711
#pragma omp taskwait depend(in : *clique_signal)
26782712
return cut_pass_result.status;
@@ -2695,8 +2729,18 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
26952729
}
26962730
}
26972731

2732+
// Publish the post-cuts root LP value.
2733+
if (settings_.benchmark_info_ptr != nullptr) {
2734+
settings_.benchmark_info_ptr->root_lp_with_cuts =
2735+
compute_user_objective(original_lp_, root_objective_);
2736+
}
2737+
26982738
print_cut_info(settings_, cut_info);
26992739
f_t cut_generation_time = toc(cut_generation_start_time);
2740+
// Publish cut-generation time for reporting.
2741+
if (settings_.benchmark_info_ptr != nullptr) {
2742+
settings_.benchmark_info_ptr->cut_generation_time_sec = cut_generation_time;
2743+
}
27002744
if (cut_info.has_cuts()) {
27012745
settings_.log.printf("Cut generation time: %.2f seconds\n", cut_generation_time);
27022746
settings_.log.printf("Cut pool size : %d\n", cut_pool_size);

0 commit comments

Comments
 (0)