Skip to content

Commit a57d894

Browse files
authored
Integrate CliqueMerging presolver and tune presolver settings (#415)
This PR integrates the CliqueMerging presolver from Papilo and customizes some presolver settings for cuOpt. This PR also provides `--dual-postsolve` option. This option allows users to disable dual post solve which makes presolver apply more reductions. By default this flag is set to false. ## Issue Closes #356 Authors: - Rajesh Gandham (https://github.com/rg20) - Ramakrishnap (https://github.com/rgsl888prabhu) Approvers: - Alice Boucher (https://github.com/aliceb-nv) - Trevor McKay (https://github.com/tmckayus) - Ramakrishnap (https://github.com/rgsl888prabhu) URL: #415
1 parent a94cf40 commit a57d894

File tree

21 files changed

+124
-35
lines changed

21 files changed

+124
-35
lines changed

ci/test_wheel_cuopt.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,8 @@ timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh
7373
# Run Python tests
7474
RAPIDS_DATASET_ROOT_DIR=./datasets timeout 30m python -m pytest --verbose --capture=no ./python/cuopt/cuopt/tests/
7575

76-
# run cvxpy integration tests
77-
./ci/thirdparty-testing/run_cvxpy_tests.sh
78-
79-
# run jump tests for only nightly builds
76+
# run jump tests and cvxpy integration tests for only nightly builds
8077
if [[ "${RAPIDS_BUILD_TYPE}" == "nightly" ]]; then
8178
./ci/thirdparty-testing/run_jump_tests.sh
79+
./ci/thirdparty-testing/run_cvxpy_tests.sh
8280
fi

ci/utils/install_boost_tbb.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ if [ -f /etc/os-release ]; then
2424
echo "Detected Rocky Linux. Installing Boost and TBB via dnf..."
2525
dnf clean all
2626
dnf -y update
27-
dnf install -y boost-devel tbb-devel
27+
dnf install -y epel-release
28+
dnf install -y boost1.78-devel tbb-devel
2829
if [[ "$(uname -m)" == "x86_64" ]]; then
2930
dnf install -y gcc-toolset-14-libquadmath-devel
3031
fi

cpp/CMakeLists.txt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,14 @@ endif()
183183
FetchContent_Declare(
184184
papilo
185185
GIT_REPOSITORY "https://github.com/scipopt/papilo.git"
186-
GIT_TAG "v2.4.3"
186+
# We would want to get the main branch. However, the main branch
187+
# does not have some of the presolvers and settings that we need
188+
# Mainly, probing and clique merging.
189+
# This is the reason we are using the development branch
190+
# commit from Sep 26, 2025. Once these changes are merged into the main branch,
191+
#we can switch to the main branch.
192+
GIT_TAG "34a40781fa14f8870cb6368cffb6c0eda2f47511"
193+
GIT_PROGRESS TRUE
187194
SYSTEM
188195
)
189196

@@ -422,7 +429,6 @@ if(NOT BUILD_LP_ONLY)
422429
cuopt
423430
OpenMP::OpenMP_CXX
424431
PRIVATE
425-
papilo-core
426432
)
427433
set_property(TARGET cuopt_cli PROPERTY INSTALL_RPATH "$ORIGIN/../${lib_dir}")
428434

@@ -446,7 +452,6 @@ if(BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY)
446452
cuopt
447453
OpenMP::OpenMP_CXX
448454
PRIVATE
449-
papilo-core
450455
)
451456
endif()
452457

@@ -462,7 +467,6 @@ if(BUILD_LP_BENCHMARKS)
462467
cuopt
463468
OpenMP::OpenMP_CXX
464469
PRIVATE
465-
papilo-core
466470
)
467471
endif()
468472

cpp/include/cuopt/linear_programming/constants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
#define CUOPT_LOG_TO_CONSOLE "log_to_console"
5252
#define CUOPT_CROSSOVER "crossover"
5353
#define CUOPT_PRESOLVE "presolve"
54+
#define CUOPT_DUAL_POSTSOLVE "dual_postsolve"
5455
#define CUOPT_MIP_ABSOLUTE_TOLERANCE "mip_absolute_tolerance"
5556
#define CUOPT_MIP_RELATIVE_TOLERANCE "mip_relative_tolerance"
5657
#define CUOPT_MIP_INTEGRALITY_TOLERANCE "mip_integrality_tolerance"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class pdlp_solver_settings_t {
211211
bool save_best_primal_so_far{false};
212212
bool first_primal_feasible{false};
213213
bool presolve{false};
214+
bool dual_postsolve{true};
214215
method_t method{method_t::Concurrent};
215216
// For concurrent termination
216217
std::atomic<i_t>* concurrent_halt;

cpp/src/dual_simplex/branch_and_bound.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,8 +567,6 @@ dual::status_t branch_and_bound_t<i_t, f_t>::node_dual_simplex(
567567
leaf_problem, lp_start_time, lp_settings, leaf_solution, leaf_vstatus, leaf_edge_norms);
568568
lp_status = convert_lp_status_to_dual_status(second_status);
569569
}
570-
} else {
571-
log.printf("Infeasible after bounds strengthening. Fathoming node %d.\n", leaf_id);
572570
}
573571

574572
mutex_stats_.lock();

cpp/src/linear_programming/solve.cu

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -700,11 +700,12 @@ optimization_problem_solution_t<i_t, f_t> solve_lp(optimization_problem_t<i_t, f
700700
if (run_presolve) {
701701
// allocate no more than 10% of the time limit to presolve.
702702
// Note that this is not the presolve time, but the time limit for presolve.
703-
const double presolve_time_limit = 0.1 * lp_timer.remaining_time();
703+
const double presolve_time_limit = std::min(0.1 * lp_timer.remaining_time(), 60.0);
704704
presolver = std::make_unique<detail::third_party_presolve_t<i_t, f_t>>();
705705
auto [reduced_problem, feasible] =
706706
presolver->apply(op_problem,
707707
cuopt::linear_programming::problem_category_t::LP,
708+
settings.dual_postsolve,
708709
settings.tolerances.absolute_primal_tolerance,
709710
settings.tolerances.relative_primal_tolerance,
710711
presolve_time_limit);
@@ -714,7 +715,7 @@ optimization_problem_solution_t<i_t, f_t> solve_lp(optimization_problem_t<i_t, f
714715
}
715716
problem = detail::problem_t<i_t, f_t>(reduced_problem);
716717
presolve_time = lp_timer.elapsed_time();
717-
CUOPT_LOG_INFO("Third party presolve time: %f", presolve_time);
718+
CUOPT_LOG_INFO("Papilo presolve time: %f", presolve_time);
718719
}
719720

720721
CUOPT_LOG_INFO(

cpp/src/math_optimization/solver_settings.cu

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ solver_settings_t<i_t, f_t>::solver_settings_t() : pdlp_settings(), mip_settings
107107
{CUOPT_LOG_TO_CONSOLE, &mip_settings.log_to_console, true},
108108
{CUOPT_CROSSOVER, &pdlp_settings.crossover, false},
109109
{CUOPT_PRESOLVE, &pdlp_settings.presolve, false},
110-
{CUOPT_PRESOLVE, &mip_settings.presolve, true}
110+
{CUOPT_PRESOLVE, &mip_settings.presolve, true},
111+
{CUOPT_DUAL_POSTSOLVE, &pdlp_settings.dual_postsolve, true}
111112
};
112113
// String parameters
113114
string_parameters = {

cpp/src/mip/presolve/third_party_presolve.cpp

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ static papilo::PostsolveStorage<double> post_solve_storage_;
3434
static bool maximize_ = false;
3535

3636
template <typename i_t, typename f_t>
37-
papilo::Problem<f_t> build_papilo_problem(const optimization_problem_t<i_t, f_t>& op_problem)
37+
papilo::Problem<f_t> build_papilo_problem(const optimization_problem_t<i_t, f_t>& op_problem,
38+
problem_category_t category)
3839
{
3940
// Build papilo problem from optimization problem
4041
papilo::ProblemBuilder<f_t> builder;
@@ -167,7 +168,11 @@ papilo::Problem<f_t> build_papilo_problem(const optimization_problem_t<i_t, f_t>
167168

168169
if (h_entries.size()) {
169170
auto constexpr const sorted_entries = true;
170-
auto csr_storage = papilo::SparseStorage<f_t>(h_entries, num_rows, num_cols, sorted_entries);
171+
// MIP reductions like clique merging and substituition require more fillin
172+
const double spare_ratio = category == problem_category_t::MIP ? 4.0 : 2.0;
173+
const int min_inter_row_space = category == problem_category_t::MIP ? 30 : 4;
174+
auto csr_storage = papilo::SparseStorage<f_t>(
175+
h_entries, num_rows, num_cols, sorted_entries, spare_ratio, min_inter_row_space);
171176
problem.setConstraintMatrix(csr_storage, h_constr_lb, h_constr_ub, h_row_flags);
172177

173178
papilo::ConstraintMatrix<f_t>& matrix = problem.getConstraintMatrix();
@@ -304,14 +309,16 @@ void check_postsolve_status(const papilo::PostsolveStatus& status)
304309
}
305310

306311
template <typename f_t>
307-
void set_presolve_methods(papilo::Presolve<f_t>& presolver, problem_category_t category)
312+
void set_presolve_methods(papilo::Presolve<f_t>& presolver,
313+
problem_category_t category,
314+
bool dual_postsolve)
308315
{
309316
using uptr = std::unique_ptr<papilo::PresolveMethod<f_t>>;
310317

311-
// cuopt custom presolvers
312-
if (category == problem_category_t::MIP)
318+
if (category == problem_category_t::MIP) {
319+
// cuOpt custom GF2 presolver
313320
presolver.addPresolveMethod(uptr(new cuopt::linear_programming::detail::GF2Presolve<f_t>()));
314-
321+
}
315322
// fast presolvers
316323
presolver.addPresolveMethod(uptr(new papilo::SingletonCols<f_t>()));
317324
presolver.addPresolveMethod(uptr(new papilo::CoefficientStrengthening<f_t>()));
@@ -326,16 +333,21 @@ void set_presolve_methods(papilo::Presolve<f_t>& presolver, problem_category_t c
326333
presolver.addPresolveMethod(uptr(new papilo::SingletonStuffing<f_t>()));
327334
presolver.addPresolveMethod(uptr(new papilo::DualFix<f_t>()));
328335
presolver.addPresolveMethod(uptr(new papilo::SimplifyInequalities<f_t>()));
336+
presolver.addPresolveMethod(uptr(new papilo::CliqueMerging<f_t>()));
329337

330338
// exhaustive presolvers
331339
presolver.addPresolveMethod(uptr(new papilo::ImplIntDetection<f_t>()));
332340
presolver.addPresolveMethod(uptr(new papilo::DominatedCols<f_t>()));
333341
presolver.addPresolveMethod(uptr(new papilo::Probing<f_t>()));
334342

335-
presolver.addPresolveMethod(uptr(new papilo::DualInfer<f_t>));
336-
presolver.addPresolveMethod(uptr(new papilo::SimpleSubstitution<f_t>()));
337-
presolver.addPresolveMethod(uptr(new papilo::Sparsify<f_t>()));
338-
presolver.addPresolveMethod(uptr(new papilo::Substitution<f_t>()));
343+
if (!dual_postsolve) {
344+
presolver.addPresolveMethod(uptr(new papilo::DualInfer<f_t>()));
345+
presolver.addPresolveMethod(uptr(new papilo::SimpleSubstitution<f_t>()));
346+
presolver.addPresolveMethod(uptr(new papilo::Sparsify<f_t>()));
347+
presolver.addPresolveMethod(uptr(new papilo::Substitution<f_t>()));
348+
} else {
349+
CUOPT_LOG_INFO("Disabling the presolver methods that do not support dual postsolve");
350+
}
339351
}
340352

341353
template <typename i_t, typename f_t>
@@ -351,26 +363,51 @@ void set_presolve_options(papilo::Presolve<f_t>& presolver,
351363
presolver.getPresolveOptions().feastol = 1e-5;
352364
}
353365

366+
template <typename f_t>
367+
void set_presolve_parameters(papilo::Presolve<f_t>& presolver,
368+
problem_category_t category,
369+
int nrows,
370+
int ncols)
371+
{
372+
// It looks like a copy. But this copy has the pointers to relevant variables in papilo
373+
auto params = presolver.getParameters();
374+
if (category == problem_category_t::MIP) {
375+
// Papilo has work unit measurements for probing. Because of this when the first batch fails to
376+
// produce any reductions, the algorithm stops. To avoid stopping the algorithm, we set a
377+
// minimum badge size to a huge value. The time limit makes sure that we exit if it takes too
378+
// long
379+
int min_badgesize = std::max(ncols / 2, 32);
380+
params.setParameter("probing.minbadgesize", min_badgesize);
381+
params.setParameter("cliquemerging.enabled", true);
382+
params.setParameter("cliquemerging.maxcalls", 50);
383+
}
384+
}
385+
354386
template <typename i_t, typename f_t>
355387
std::pair<optimization_problem_t<i_t, f_t>, bool> third_party_presolve_t<i_t, f_t>::apply(
356388
optimization_problem_t<i_t, f_t> const& op_problem,
357389
problem_category_t category,
390+
bool dual_postsolve,
358391
f_t absolute_tolerance,
359392
f_t relative_tolerance,
360393
double time_limit,
361394
i_t num_cpu_threads)
362395
{
363-
papilo::Problem<f_t> papilo_problem = build_papilo_problem(op_problem);
396+
papilo::Problem<f_t> papilo_problem = build_papilo_problem(op_problem, category);
364397

365398
CUOPT_LOG_INFO("Unpresolved problem:: %d constraints, %d variables, %d nonzeros",
366399
papilo_problem.getNRows(),
367400
papilo_problem.getNCols(),
368401
papilo_problem.getConstraintMatrix().getNnz());
369402

403+
CUOPT_LOG_INFO("Calling Papilo presolver");
404+
if (category == problem_category_t::MIP) { dual_postsolve = false; }
370405
papilo::Presolve<f_t> presolver;
371-
set_presolve_methods<f_t>(presolver, category);
406+
set_presolve_methods<f_t>(presolver, category, dual_postsolve);
372407
set_presolve_options<i_t, f_t>(
373408
presolver, category, absolute_tolerance, relative_tolerance, time_limit, num_cpu_threads);
409+
set_presolve_parameters<f_t>(
410+
presolver, category, op_problem.get_n_constraints(), op_problem.get_n_variables());
374411

375412
// Disable papilo logs
376413
presolver.setVerbosityLevel(papilo::VerbosityLevel::kQuiet);
@@ -423,9 +460,12 @@ void third_party_presolve_t<i_t, f_t>::undo(rmm::device_uvector<f_t>& primal_sol
423460
check_postsolve_status(status);
424461

425462
primal_solution.resize(full_sol.primal.size(), stream_view);
426-
dual_solution.resize(full_sol.primal.size(), stream_view);
427-
reduced_costs.resize(full_sol.primal.size(), stream_view);
463+
dual_solution.resize(full_sol.dual.size(), stream_view);
464+
reduced_costs.resize(full_sol.reducedCosts.size(), stream_view);
428465
raft::copy(primal_solution.data(), full_sol.primal.data(), full_sol.primal.size(), stream_view);
466+
raft::copy(dual_solution.data(), full_sol.dual.data(), full_sol.dual.size(), stream_view);
467+
raft::copy(
468+
reduced_costs.data(), full_sol.reducedCosts.data(), full_sol.reducedCosts.size(), stream_view);
429469
}
430470

431471
#if MIP_INSTANTIATE_FLOAT

cpp/src/mip/presolve/third_party_presolve.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class third_party_presolve_t {
2929
std::pair<optimization_problem_t<i_t, f_t>, bool> apply(
3030
optimization_problem_t<i_t, f_t> const& op_problem,
3131
problem_category_t category,
32+
bool dual_postsolve,
3233
f_t absolute_tolerance,
3334
f_t relative_tolerance,
3435
double time_limit,

0 commit comments

Comments
 (0)