Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d65be83
main baseline test
akifcorduk Apr 24, 2026
eb5d586
fix thread count
akifcorduk Apr 24, 2026
056552f
Merge branch 'main' of github.com:NVIDIA/cuopt into main_baselin
akifcorduk May 7, 2026
e7bf32c
with gap computation
akifcorduk May 7, 2026
5335b65
measure main branch
akifcorduk May 8, 2026
0b04683
test clique changes
akifcorduk May 12, 2026
bc5006f
remove deterministic guards
akifcorduk May 13, 2026
af65630
fix complement bug
akifcorduk May 15, 2026
3f0ace1
fix omp
akifcorduk May 15, 2026
7995451
with additional fix
akifcorduk May 16, 2026
97e439b
Merge upstream/main into main_baselin
akifcorduk May 17, 2026
8ad61dd
add cuda error recovery for capture
akifcorduk May 20, 2026
0a5149b
test CI
akifcorduk May 20, 2026
06352db
fix logger
akifcorduk May 20, 2026
36e74d2
Merge branch 'main' of github.com:NVIDIA/cuopt into cuda_graph_side_c…
akifcorduk May 20, 2026
4d2fb18
restore the api and use api suitable for <12.3
akifcorduk May 20, 2026
dba39c8
more comments
akifcorduk May 20, 2026
0a4571f
Merge branch 'cuda_graph_side_capture' into main_baselin
akifcorduk May 20, 2026
56b6e84
fix ping pong graph major, non-major logic
akifcorduk May 20, 2026
b83836b
Merge branch 'cuda_graph_side_capture' into main_baselin
akifcorduk May 20, 2026
ce6d499
Merge branch 'main' of github.com:NVIDIA/cuopt into main_Baselin
akifcorduk Jun 1, 2026
4de79a0
fix timer, better jaccard
akifcorduk Jun 2, 2026
a17ffa3
simplify comments
akifcorduk Jun 2, 2026
e5bc991
cut stats
akifcorduk Jun 2, 2026
b5bd4b2
Revert PDLP/PDHG cuda-graph changes to baseline
akifcorduk Jun 2, 2026
947fc63
Merge branch 'main_with_stats' into main_baselin
akifcorduk Jun 2, 2026
c35573a
revery cpu affinity mapping
akifcorduk Jun 2, 2026
85a4024
try without cousin filter
akifcorduk Jun 3, 2026
92d9996
remove cut configs and cousin logic
akifcorduk Jun 3, 2026
14b0ab8
Separate small adjacency-list clique cuts
akifcorduk Jun 3, 2026
ee7f2b5
revert omp inlining
akifcorduk Jun 3, 2026
98b1ee2
remove patch
akifcorduk Jun 3, 2026
59a05ac
handle ai review
akifcorduk Jun 5, 2026
6a75d1e
fix tests
akifcorduk Jun 5, 2026
b3e0eef
fix timer stats
akifcorduk Jun 5, 2026
bd4228d
remove comments
akifcorduk Jun 5, 2026
dd69485
handle reviewer comments
akifcorduk Jun 9, 2026
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
464 changes: 464 additions & 0 deletions benchmarks/linear_programming/cuopt/miplib2017_bks.hpp

Large diffs are not rendered by default.

71 changes: 57 additions & 14 deletions benchmarks/linear_programming/cuopt/run_mip.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/* clang-format on */
#include "initial_solution_reader.hpp"
#include "mip_test_instances.hpp"
#include "miplib2017_bks.hpp"

#include <cstdio>
#include <cuopt/linear_programming/io/parser.hpp>
Expand Down Expand Up @@ -239,6 +240,43 @@ int run_single_file(std::string file_path,
} else {
CUOPT_LOG_INFO("%s: no solution found", base_filename.c_str());
}

// Per-instance "gap closed to BKS" stat. Emits a single
// grep-friendly "MIPLIBGapStat ..." line via printf so cross-branch
// comparison is just `grep '^MIPLIBGapStat' branchA.log` then diff.
// BKS values are looked up from the in-source MIPLIB2017 benchmark-set
// table (miplib2017_bks.hpp); unknown instances emit "opt=TBD"
// and infeasibility-flagged instances emit "opt=Infeasible".
{
const double _gap_seconds = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start_run_solver)
.count() /
1000.0;
std::string _status_str;
switch (solution.get_termination_status()) {
case cuopt::linear_programming::mip_termination_status_t::Optimal:
_status_str = "Optimal";
break;
case cuopt::linear_programming::mip_termination_status_t::FeasibleFound:
_status_str = "FeasibleFound";
break;
case cuopt::linear_programming::mip_termination_status_t::TimeLimit:
_status_str = "TimeLimit";
break;
case cuopt::linear_programming::mip_termination_status_t::Infeasible:
_status_str = "Infeasible";
break;
default: _status_str = "Other"; break;
}
cuopt_bench::print_miplib_gap_stat(base_filename,
solution,
_gap_seconds,
_status_str,
benchmark_info.root_lp_no_cuts,
benchmark_info.root_lp_with_cuts,
benchmark_info.cut_generation_time_sec);
}

std::stringstream ss;
int decimal_places = 2;
double mip_gap = solution.get_mip_gap();
Expand Down Expand Up @@ -534,31 +572,36 @@ int main(int argc, char* argv[])
merge_result_files(out_dir, result_file, n_gpus, batch_num);
} else {
auto memory_resource = make_async();
auto run_single = [&]() {
run_single_file(path,
0,
0,
n_gpus,
out_dir,
initial_solution_file,
heuristics_only,
num_cpu_threads,
write_log_file,
log_to_console,
reliability_branching,
time_limit,
work_limit,
deterministic);
};
if (memory_limit > 0) {
auto limiting_adaptor =
rmm::mr::limiting_resource_adaptor(memory_resource, memory_limit * 1024ULL * 1024ULL);
rmm::mr::set_current_device_resource(limiting_adaptor);
run_single();
} else if (track_allocations) {
rmm::mr::tracking_resource_adaptor tracking_adaptor(memory_resource,
/*capture_stacks=*/true);
rmm::mr::set_current_device_resource(tracking_adaptor);
run_single();
} else {
rmm::mr::set_current_device_resource(memory_resource);
run_single();
}
run_single_file(path,
0,
0,
n_gpus,
out_dir,
initial_solution_file,
heuristics_only,
num_cpu_threads,
write_log_file,
log_to_console,
reliability_branching,
time_limit,
work_limit,
deterministic);
}

return 0;
Expand Down
21 changes: 21 additions & 0 deletions cpp/include/cuopt/linear_programming/mip/solver_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ struct benchmark_info_t {
double last_improvement_of_best_feasible = 0;
double last_improvement_after_recombination = 0;
double objective_of_initial_population = std::numeric_limits<double>::max();
// LP relaxation objective at the root node, BEFORE any cuts have been
// added. quiet_NaN() means "B&B did not run cut passes / value was
// never written" — distinguishes it from a legitimate 0.0.
double root_lp_no_cuts = std::numeric_limits<double>::quiet_NaN();
// LP relaxation objective at the root node, AFTER the full cut loop
// (final pass result). The dual gap "by cuts at the root" is then
// gap_after_cuts = opt - root_lp_with_cuts (in B&B's solver
// objective sense)
// and the classical "gap closed by cuts" metric is
// gap_closed_pct = 100 * (root_lp_with_cuts - root_lp_no_cuts)
// / (opt - root_lp_no_cuts).
// quiet_NaN() means "B&B did not finish the cut loop / value not written".
Comment thread
coderabbitai[bot] marked this conversation as resolved.
double root_lp_with_cuts = std::numeric_limits<double>::quiet_NaN();

// Wall-clock time spent inside the root-node cut generation loop
// (sum of generate_cuts + score_cuts + check_for_duplicate_cuts +
// get_best_cuts + add_cuts + post-cut LP resolves), in seconds.
// Published by branch_and_bound.cpp::solve() at the same point that
// root_lp_with_cuts is finalised. quiet_NaN() means "cut loop did
// not run / value never written".
double cut_generation_time_sec = std::numeric_limits<double>::quiet_NaN();
};

// Forward declare solver_settings_t for friend class
Expand Down
46 changes: 45 additions & 1 deletion cpp/src/branch_and_bound/branch_and_bound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <branch_and_bound/pseudo_costs.hpp>
#include <branch_and_bound/symmetry.hpp>

#include <cuopt/linear_programming/mip/solver_settings.hpp> // benchmark_info_t

#include <cuts/cuts.hpp>
#include <mip_heuristics/feasibility_jump/cpu_fj_thread.cuh>
#include <mip_heuristics/mip_constants.hpp>
Expand Down Expand Up @@ -2361,6 +2363,11 @@ auto branch_and_bound_t<i_t, f_t>::do_cut_pass(
}
root_objective_ = compute_objective(original_lp_, root_relax_soln_.x);

if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->root_lp_with_cuts =
compute_user_objective(original_lp_, root_objective_);
}

f_t remove_cuts_start_time = tic();
mutex_original_lp_.lock();
remove_cuts(original_lp_,
Expand Down Expand Up @@ -2479,7 +2486,7 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
user_problem_t<i_t, f_t> problem_copy = original_problem_;
timer_t timer(std::numeric_limits<double>::infinity());
detail::find_initial_cliques(
problem_copy, tolerances_for_clique, &clique_table_, timer, false, clique_signal);
problem_copy, tolerances_for_clique, &clique_table_, timer, clique_signal);
}
}

Expand Down Expand Up @@ -2588,6 +2595,11 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
cut_info_t<i_t, f_t> cut_info;

if (num_fractional == 0) {
if (settings_.benchmark_info_ptr != nullptr) {
const double v = static_cast<double>(compute_user_objective(original_lp_, root_objective_));
settings_.benchmark_info_ptr->root_lp_no_cuts = v;
settings_.benchmark_info_ptr->root_lp_with_cuts = v;
}
set_solution_at_root(solution, cut_info);
signal_extend_cliques_.store(true, std::memory_order_release);
#pragma omp taskwait depend(in : *clique_signal)
Expand Down Expand Up @@ -2624,6 +2636,15 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
f_t last_objective = root_objective_;
f_t root_relax_objective = root_objective_;

// Publish the no-cuts root LP value once. The with-cuts companion is
// published below after the cut loop terminates. Both go to the
// benchmark_info_t so callers (run_mip.cpp) can compute
// gap-closed-by-cuts without instrumenting the cut loop directly.
if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->root_lp_no_cuts =
compute_user_objective(original_lp_, root_relax_objective);
}

constexpr bool enable_root_cut_cpufj = true;
std::unique_ptr<detail::fj_cpu_task_t<i_t, f_t>> root_cut_cpufj_task;
auto root_cut_cpufj_improvement_callback =
Expand Down Expand Up @@ -2652,7 +2673,17 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
i_t cut_pool_size = 0;
for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) {
if (num_fractional == 0) {
// LP relaxation is already integer-feasible — solved at the root
// by the cuts added so far (possibly zero). Publish the with-cuts
// value so the gap-closed line still has a non-NaN dual bound.
if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->root_lp_with_cuts =
compute_user_objective(original_lp_, root_objective_);
}
Comment on lines +2679 to +2682

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any scenario where benchmark_info_ptr != nullptr? We're just computing simple scalars, I think it'd make the code easier to read if it was just a regular reference / non-null ptr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also nit but I tend to prefer implicit casts / C-style casts for non-critical things :) static_cast(...) is kinda harder to read

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lambda encapsulates the logic of cut time and writing to benchmark info together. But okay let me remove the lambda and handle it on return blocks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production code, it benchmark_info_ptr != nullptr doesn't happen but it happens when we run benchmark with run_mip. I think it is irrelevant to the user and I wanted to separate it from the rest of the user settings.

For the cast, AI did the cast, I also prefer C style casts for simple static casts. Will corrrect it.

set_solution_at_root(solution, cut_info);
if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time);
}
signal_extend_cliques_.store(true, std::memory_order_release);
#pragma omp taskwait depend(in : *clique_signal)
return mip_status_t::OPTIMAL;
Expand Down Expand Up @@ -2692,6 +2723,9 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
}

if (cut_pass_result.action == cut_pass_action_t::RETURN) {
if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->cut_generation_time_sec = toc(cut_generation_start_time);
}
signal_extend_cliques_.store(true, std::memory_order_release);
#pragma omp taskwait depend(in : *clique_signal)
return cut_pass_result.status;
Expand All @@ -2714,8 +2748,18 @@ mip_status_t branch_and_bound_t<i_t, f_t>::solve(mip_solution_t<i_t, f_t>& solut
}
}

// Publish the post-cuts root LP value.
if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->root_lp_with_cuts =
compute_user_objective(original_lp_, root_objective_);
}

print_cut_info(settings_, cut_info);
f_t cut_generation_time = toc(cut_generation_start_time);
// Publish cut-generation time for reporting.
if (settings_.benchmark_info_ptr != nullptr) {
settings_.benchmark_info_ptr->cut_generation_time_sec = cut_generation_time;
}
if (cut_info.has_cuts()) {
settings_.log.printf("Cut generation time: %.2f seconds\n", cut_generation_time);
settings_.log.printf("Cut pool size : %d\n", cut_pool_size);
Expand Down
Loading
Loading