Skip to content

Commit 3f688aa

Browse files
authored
Papilo primal/dual crush (#1104)
## Issue Authors: - Alice Boucher (https://github.com/aliceb-nv) Approvers: - Nicolas L. Guidotti (https://github.com/nguidotti) - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) URL: #1104
1 parent 679d7ab commit 3f688aa

8 files changed

Lines changed: 954 additions & 19 deletions

File tree

cpp/src/mip_heuristics/diversity/diversity_manager.cu

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "diversity_manager.cuh"
1010

1111
#include <mip_heuristics/mip_constants.hpp>
12+
#include <mip_heuristics/presolve/third_party_presolve.hpp>
1213

1314
#include <mip_heuristics/presolve/conflict_graph/clique_table.cuh>
1415
#include <mip_heuristics/presolve/probing_cache.cuh>
@@ -182,9 +183,33 @@ void diversity_manager_t<i_t, f_t>::add_user_given_solutions(
182183
std::vector<solution_t<i_t, f_t>>& initial_sol_vector)
183184
{
184185
raft::common::nvtx::range fun_scope("add_user_given_solutions");
185-
for (const auto& init_sol : context.settings.initial_solutions) {
186+
const bool has_papilo = problem_ptr->has_papilo_presolve_data();
187+
const i_t papilo_orig_n = problem_ptr->get_papilo_original_num_variables();
188+
for (size_t sol_idx = 0; sol_idx < context.settings.initial_solutions.size(); ++sol_idx) {
189+
const auto& init_sol = context.settings.initial_solutions[sol_idx];
186190
solution_t<i_t, f_t> sol(*problem_ptr);
187191
rmm::device_uvector<f_t> init_sol_assignment(*init_sol, sol.handle_ptr->get_stream());
192+
193+
if (has_papilo) {
194+
if ((i_t)init_sol_assignment.size() != papilo_orig_n) {
195+
CUOPT_LOG_ERROR(
196+
"Error cannot add the provided initial solution! Initial solution %zu has %zu vars, "
197+
"expected %d; skipping",
198+
sol_idx,
199+
init_sol_assignment.size(),
200+
papilo_orig_n);
201+
continue;
202+
}
203+
std::vector<f_t> h_original = host_copy(init_sol_assignment, sol.handle_ptr->get_stream());
204+
std::vector<f_t> h_crushed;
205+
problem_ptr->presolve_data.papilo_presolve_ptr->crush_primal_solution(h_original, h_crushed);
206+
init_sol_assignment = cuopt::device_copy(h_crushed, sol.handle_ptr->get_stream());
207+
CUOPT_LOG_DEBUG("Crushed initial solution %d through Papilo (%d -> %d vars)",
208+
sol_idx,
209+
papilo_orig_n,
210+
h_crushed.size());
211+
}
212+
188213
if (problem_ptr->pre_process_assignment(init_sol_assignment)) {
189214
relaxed_lp_settings_t lp_settings;
190215
lp_settings.time_limit = std::min(60., timer.remaining_time() / 2);
@@ -202,10 +227,10 @@ void diversity_manager_t<i_t, f_t>::add_user_given_solutions(
202227
sol.handle_ptr->get_stream());
203228
bool is_feasible = sol.compute_feasibility();
204229
cuopt_func_call(sol.test_variable_bounds(true));
205-
CUOPT_LOG_INFO("Adding initial solution success! feas %d objective %f excess %f",
206-
is_feasible,
207-
sol.get_user_objective(),
208-
sol.get_total_excess());
230+
CUOPT_LOG_DEBUG("Adding initial solution success! feas %d objective %f excess %f",
231+
is_feasible,
232+
sol.get_user_objective(),
233+
sol.get_total_excess());
209234
population.run_solution_callbacks(sol);
210235
initial_sol_vector.emplace_back(std::move(sol));
211236
} else {
@@ -424,6 +449,9 @@ solution_t<i_t, f_t> diversity_manager_t<i_t, f_t>::run_solver()
424449
CUOPT_LOG_INFO("GPU heuristics disabled via CUOPT_DISABLE_GPU_HEURISTICS=1");
425450
population.initialize_population();
426451
population.allocate_solutions();
452+
add_user_given_solutions(initial_sol_vector);
453+
population.add_solutions_from_vec(std::move(initial_sol_vector));
454+
if (check_b_b_preemption()) { return population.best_feasible(); }
427455

428456
while (!check_b_b_preemption()) {
429457
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -454,8 +482,9 @@ solution_t<i_t, f_t> diversity_manager_t<i_t, f_t>::run_solver()
454482
"The problem must not be ii");
455483
population.initialize_population();
456484
population.allocate_solutions();
457-
if (check_b_b_preemption()) { return population.best_feasible(); }
458485
add_user_given_solutions(initial_sol_vector);
486+
population.add_solutions_from_vec(std::move(initial_sol_vector));
487+
if (check_b_b_preemption()) { return population.best_feasible(); }
459488
// Run CPUFJ early to find quick initial solutions
460489
ls_cpufj_raii_guard_t ls_cpufj_raii_guard(ls); // RAII to stop cpufj threads on solve stop
461490
ls.start_cpufj_scratch_threads(population);
@@ -612,8 +641,6 @@ solution_t<i_t, f_t> diversity_manager_t<i_t, f_t>::run_solver()
612641
ls.start_cpufj_lptopt_scratch_threads(population);
613642
}
614643

615-
population.add_solutions_from_vec(std::move(initial_sol_vector));
616-
617644
if (check_b_b_preemption()) { return population.best_feasible(); }
618645

619646
if (context.settings.benchmark_info_ptr != nullptr) {

cpp/src/mip_heuristics/presolve/third_party_presolve.cpp

Lines changed: 256 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444

4545
#include <raft/core/nvtx.hpp>
4646

47+
#include <unordered_map>
48+
4749
namespace cuopt::mathematical_optimization::mip {
4850

4951
template <typename i_t, typename f_t>
@@ -137,8 +139,10 @@ papilo::Problem<f_t> build_papilo_problem(const optimization_problem_t<i_t, f_t>
137139
}
138140
}
139141

140-
for (size_t i = 0; i < h_var_types.size(); ++i) {
141-
builder.setColIntegral(i, h_var_types[i] == var_t::INTEGER);
142+
if (category == problem_category_t::MIP) {
143+
for (size_t i = 0; i < h_var_types.size(); ++i) {
144+
builder.setColIntegral(i, h_var_types[i] == var_t::INTEGER);
145+
}
142146
}
143147

144148
if (!h_constr_lb.empty() && !h_constr_ub.empty()) {
@@ -563,8 +567,6 @@ void set_presolve_methods(papilo::Presolve<f_t>& presolver,
563567
presolver.addPresolveMethod(uptr(new papilo::SimpleProbing<f_t>()));
564568
presolver.addPresolveMethod(uptr(new papilo::ParallelRowDetection<f_t>()));
565569
presolver.addPresolveMethod(uptr(new papilo::ParallelColDetection<f_t>()));
566-
567-
presolver.addPresolveMethod(uptr(new papilo::SingletonStuffing<f_t>()));
568570
presolver.addPresolveMethod(uptr(new papilo::DualFix<f_t>()));
569571
presolver.addPresolveMethod(uptr(new papilo::SimplifyInequalities<f_t>()));
570572
presolver.addPresolveMethod(uptr(new papilo::CliqueMerging<f_t>()));
@@ -575,6 +577,11 @@ void set_presolve_methods(papilo::Presolve<f_t>& presolver,
575577
presolver.addPresolveMethod(uptr(new papilo::Probing<f_t>()));
576578

577579
if (!dual_postsolve) {
580+
// SingletonStuffing causes dual crushing failures on:
581+
// tr12-30, ns1208400, gmu-35-50, dws008-01, neos-1445765,
582+
// neos-5107597-kakapo, rocI-4-11, traininstance2, traininstance6,
583+
// radiationm18-12-05, rococoB10-011000, b1c1s1
584+
presolver.addPresolveMethod(uptr(new papilo::SingletonStuffing<f_t>()));
578585
presolver.addPresolveMethod(uptr(new papilo::DualInfer<f_t>()));
579586
presolver.addPresolveMethod(uptr(new papilo::SimpleSubstitution<f_t>()));
580587
presolver.addPresolveMethod(uptr(new papilo::Sparsify<f_t>()));
@@ -877,6 +884,251 @@ void third_party_presolve_t<i_t, f_t>::uncrush_primal_solution(
877884
full_primal = std::move(full_sol.primal);
878885
}
879886

887+
template <typename i_t, typename f_t>
888+
void third_party_presolve_t<i_t, f_t>::crush_primal_solution(
889+
const std::vector<f_t>& original_primal, std::vector<f_t>& reduced_primal) const
890+
{
891+
cuopt_expects(presolver_ == cuopt::mathematical_optimization::presolver_t::Papilo,
892+
error_type_t::RuntimeError,
893+
"Primal crushing is only supported for PaPILO presolve");
894+
cuopt_assert(papilo_post_solve_storage_ != nullptr, "No postsolve storage available");
895+
std::vector<f_t> unused_y, unused_z;
896+
std::vector<f_t> empty_vals;
897+
std::vector<i_t> empty_indices, empty_offsets;
898+
crush_primal_dual_solution(original_primal,
899+
{},
900+
reduced_primal,
901+
unused_y,
902+
{},
903+
unused_z,
904+
empty_vals,
905+
empty_indices,
906+
empty_offsets);
907+
}
908+
909+
/**
910+
* Crush an original-space primal+dual solution into the presolved (reduced) space.
911+
*
912+
* This is the forward counterpart of Papilo's Postsolve::undo(). It replays
913+
* each presolve reduction in forward order to transform variable/dual values,
914+
* then projects onto the surviving columns/rows via origcol/origrow_mapping.
915+
*
916+
* Only two reductions actually transform survivor coordinates:
917+
* kParallelCol — merges x[col1] into x[col2]; survivor rc is z[col2] if
918+
* nonzero, else z[col1] / scale (inverse of PaPILO postsolve)
919+
* kRowBoundChangeForcedByRow — conditionally transfers y[deleted_row] → y[kept_row]
920+
*/
921+
template <typename i_t, typename f_t>
922+
void third_party_presolve_t<i_t, f_t>::crush_primal_dual_solution(
923+
const std::vector<f_t>& x_original,
924+
const std::vector<f_t>& y_original,
925+
std::vector<f_t>& x_reduced,
926+
std::vector<f_t>& y_reduced,
927+
const std::vector<f_t>& z_original,
928+
std::vector<f_t>& z_reduced,
929+
const std::vector<f_t>& A_values,
930+
const std::vector<i_t>& A_indices,
931+
const std::vector<i_t>& A_offsets) const
932+
{
933+
cuopt_expects(presolver_ == cuopt::mathematical_optimization::presolver_t::Papilo,
934+
error_type_t::RuntimeError,
935+
"Crushing is only supported for PaPILO presolve");
936+
cuopt_assert(papilo_post_solve_storage_ != nullptr, "No postsolve storage available");
937+
938+
const auto& storage = *papilo_post_solve_storage_;
939+
const auto& types = storage.types;
940+
const auto& indices = storage.indices;
941+
const auto& values = storage.values;
942+
const auto& start = storage.start;
943+
const auto& num = storage.num;
944+
945+
cuopt_assert((int)x_original.size() == (int)storage.nColsOriginal, "");
946+
947+
const bool crush_dual = !y_original.empty();
948+
if (crush_dual) { cuopt_assert((int)y_original.size() == (int)storage.nRowsOriginal, ""); }
949+
950+
const bool crush_rc = !z_original.empty() && crush_dual;
951+
if (crush_rc) { cuopt_assert((int)z_original.size() == (int)storage.nColsOriginal, ""); }
952+
953+
std::vector<f_t> x(x_original.begin(), x_original.end());
954+
std::vector<f_t> y(y_original.begin(), y_original.end());
955+
std::vector<f_t> z(z_original.begin(), z_original.end());
956+
957+
// Track current coefficient values for entries modified by kCoefficientChange,
958+
// so repeated changes to the same (row, col) are handled correctly.
959+
std::unordered_map<i_t, f_t> coeff_current;
960+
961+
const i_t n_cols_original = (i_t)storage.nColsOriginal;
962+
963+
auto coeff_key = [&](int row, int col) -> i_t { return (i_t)row * n_cols_original + (i_t)col; };
964+
965+
auto get_coeff = [&](int row, int col) -> f_t {
966+
auto it = coeff_current.find(coeff_key(row, col));
967+
if (it != coeff_current.end()) return it->second;
968+
for (i_t p = A_offsets[row]; p < A_offsets[row + 1]; ++p) {
969+
if (A_indices[p] == col) return A_values[p];
970+
}
971+
return 0;
972+
};
973+
974+
for (int i = 0; i < (int)types.size(); ++i) {
975+
int first = start[i];
976+
977+
switch (types[i]) {
978+
case ReductionType::kParallelCol: {
979+
// Storage layout: [orig_col1, flags1, orig_col2, flags2, -1]
980+
// [col1lb, col1ub, col2lb, col2ub, col2scale]
981+
int col1 = indices[first];
982+
int col2 = indices[first + 2];
983+
const f_t& scale = values[first + 4];
984+
x[col2] += scale * x[col1];
985+
if (crush_rc) {
986+
// Inverse of Postsolve::apply_parallel_col_to_original_solution reduced-cost split.
987+
if (num.isZero(z[col2]) && !num.isZero(z[col1])) {
988+
cuopt_assert(!num.isZero(scale), "parallel column scale must be nonzero");
989+
z[col2] = z[col1] / scale;
990+
}
991+
}
992+
break;
993+
}
994+
995+
case ReductionType::kRowBoundChangeForcedByRow: {
996+
if (!crush_dual) break;
997+
cuopt_assert(i >= 1 && types[i - 1] == ReductionType::kReasonForRowBoundChangeForcedByRow,
998+
"kRowBoundChangeForcedByRow must be preceded by its reason record");
999+
1000+
bool is_lhs = indices[first] == 1;
1001+
int row = (int)values[first];
1002+
1003+
int reason_first = start[i - 1];
1004+
int deleted_row = indices[reason_first + 1];
1005+
f_t factor = values[reason_first];
1006+
cuopt_assert(factor != 0, "parallel row factor must be nonzero");
1007+
1008+
// Forward rule: if the deleted row carried dual signal that the
1009+
// reverse would have attributed to the kept row, transfer it back.
1010+
f_t candidate = y[deleted_row] / factor;
1011+
bool sign_ok = is_lhs ? num.isGT(candidate, (f_t)0) : num.isLT(candidate, (f_t)0);
1012+
1013+
if (sign_ok) {
1014+
f_t y_old = y[row];
1015+
y[row] = candidate;
1016+
// Maintain z = c - A^T y: propagate the y change into reduced costs
1017+
if (crush_rc) {
1018+
f_t delta_y = candidate - y_old;
1019+
for (i_t p = A_offsets[row]; p < A_offsets[row + 1]; ++p) {
1020+
f_t a = get_coeff(row, A_indices[p]);
1021+
z[A_indices[p]] -= delta_y * a;
1022+
}
1023+
}
1024+
}
1025+
break;
1026+
}
1027+
1028+
case ReductionType::kCoefficientChange: {
1029+
if (!crush_rc) break;
1030+
int row = indices[first];
1031+
int col = indices[first + 1];
1032+
f_t a_new = values[first];
1033+
f_t a_old = get_coeff(row, col);
1034+
coeff_current[coeff_key(row, col)] = a_new;
1035+
z[col] += (a_old - a_new) * y[row];
1036+
break;
1037+
}
1038+
1039+
case ReductionType::kSubstitutedColWithDual: {
1040+
// Singleton substitution: column j is expressed via equality row k as
1041+
// x_j = (rhs_k - Σ_{l≠j} a_kl·x_l) / a_kj
1042+
// This changes the objective for every column l in row k:
1043+
// c_red[l] = c_orig[l] - (c_j / a_kj) · a_kl
1044+
// Adjust z accordingly: Δz[l] = -(a_kl / a_kj)·z[j] - a_kl·y[k]
1045+
if (!crush_rc) break;
1046+
int row_k = indices[first]; // equality row (original space)
1047+
int row_length = (int)values[first];
1048+
// Row coefficients start at first+3
1049+
int row_coef_start = first + 3;
1050+
// Substituted column index is after the row coefficients
1051+
int col_j = indices[row_coef_start + row_length];
1052+
1053+
// Find a_kj (coefficient of col j in row k)
1054+
f_t a_kj = 0;
1055+
for (int p = 0; p < row_length; ++p) {
1056+
if (indices[row_coef_start + p] == col_j) {
1057+
a_kj = values[row_coef_start + p];
1058+
break;
1059+
}
1060+
}
1061+
if (a_kj == 0) break; // shouldn't happen
1062+
1063+
f_t z_j = z[col_j];
1064+
f_t y_k = y[row_k];
1065+
1066+
// Adjust z for each surviving column l in the equality row (l ≠ j)
1067+
for (int p = 0; p < row_length; ++p) {
1068+
int col_l = indices[row_coef_start + p];
1069+
if (col_l == col_j) continue;
1070+
f_t a_kl = values[row_coef_start + p];
1071+
z[col_l] -= (a_kl / a_kj) * z_j + a_kl * y_k;
1072+
}
1073+
break;
1074+
}
1075+
1076+
case ReductionType::kFixedCol: // Handled via projection
1077+
case ReductionType::kSubstitutedCol: // Col is dropped
1078+
case ReductionType::kFixedInfCol: // Col is dropped
1079+
case ReductionType::kVarBoundChange: // Noop
1080+
case ReductionType::kRedundantRow: // Noop
1081+
case ReductionType::kRowBoundChange: // Noop
1082+
case ReductionType::kReasonForRowBoundChangeForcedByRow: // Metadata for above
1083+
case ReductionType::kSaveRow: // Metadata
1084+
case ReductionType::kReducedBoundsCost: // Noop
1085+
case ReductionType::kColumnDualValue: // Column reduced-cost only
1086+
case ReductionType::kRowDualValue: // Handled via projection
1087+
break;
1088+
// no default: case to let the compiler yell at us if a new reduction is later introduced
1089+
}
1090+
}
1091+
1092+
const auto& col_map = storage.origcol_mapping;
1093+
const auto& row_map = storage.origrow_mapping;
1094+
1095+
// Cancel contributions from removed rows. The original-space z was
1096+
// computed as z = c - A^T y over ALL rows. The reduced-space stationarity
1097+
// only involves surviving rows, so we must add back the terms from removed
1098+
// rows: z[j] += y[i] * a_{i,j} for every removed row i with materially nonzero y[i].
1099+
if (crush_rc) {
1100+
std::vector<bool> row_survives((int)storage.nRowsOriginal, false);
1101+
for (size_t k = 0; k < row_map.size(); ++k) {
1102+
row_survives[row_map[k]] = true;
1103+
}
1104+
for (int i = 0; i < (int)storage.nRowsOriginal; ++i) {
1105+
if (row_survives[i] || num.isZero(y[i])) continue;
1106+
for (i_t p = A_offsets[i]; p < A_offsets[i + 1]; ++p) {
1107+
z[A_indices[p]] += y[i] * get_coeff(i, A_indices[p]);
1108+
}
1109+
}
1110+
}
1111+
1112+
x_reduced.resize(col_map.size());
1113+
for (size_t k = 0; k < col_map.size(); ++k) {
1114+
x_reduced[k] = x[col_map[k]];
1115+
}
1116+
1117+
if (crush_dual) {
1118+
y_reduced.resize(row_map.size());
1119+
for (size_t k = 0; k < row_map.size(); ++k) {
1120+
y_reduced[k] = y[row_map[k]];
1121+
}
1122+
}
1123+
1124+
if (crush_rc) {
1125+
z_reduced.resize(col_map.size());
1126+
for (size_t k = 0; k < col_map.size(); ++k) {
1127+
z_reduced[k] = z[col_map[k]];
1128+
}
1129+
}
1130+
}
1131+
8801132
template <typename i_t, typename f_t>
8811133
third_party_presolve_t<i_t, f_t>::~third_party_presolve_t()
8821134
{

0 commit comments

Comments
 (0)