Skip to content

Commit 66d6529

Browse files
authored
Merge pull request #534 from aliceb-nv/impl-int-store
Carry over implied integers from papilo presolve reductions
2 parents 2a814d5 + 7c168fd commit 66d6529

File tree

9 files changed

+174
-28
lines changed

9 files changed

+174
-28
lines changed

cpp/src/linear_programming/solve.cu

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -842,19 +842,18 @@ optimization_problem_solution_t<i_t, f_t> solve_lp(optimization_problem_t<i_t, f
842842
// But no less than 1 second, to avoid early timeout triggering known crashes
843843
const double presolve_time_limit =
844844
std::max(1.0, std::min(0.1 * lp_timer.remaining_time(), 60.0));
845-
presolver = std::make_unique<detail::third_party_presolve_t<i_t, f_t>>();
846-
auto [reduced_problem, feasible] =
847-
presolver->apply(op_problem,
848-
cuopt::linear_programming::problem_category_t::LP,
849-
settings.dual_postsolve,
850-
settings.tolerances.absolute_primal_tolerance,
851-
settings.tolerances.relative_primal_tolerance,
852-
presolve_time_limit);
853-
if (!feasible) {
845+
presolver = std::make_unique<detail::third_party_presolve_t<i_t, f_t>>();
846+
auto result = presolver->apply(op_problem,
847+
cuopt::linear_programming::problem_category_t::LP,
848+
settings.dual_postsolve,
849+
settings.tolerances.absolute_primal_tolerance,
850+
settings.tolerances.relative_primal_tolerance,
851+
presolve_time_limit);
852+
if (!result.has_value()) {
854853
return optimization_problem_solution_t<i_t, f_t>(
855854
pdlp_termination_status_t::PrimalInfeasible, op_problem.get_handle_ptr()->get_stream());
856855
}
857-
problem = detail::problem_t<i_t, f_t>(reduced_problem);
856+
problem = detail::problem_t<i_t, f_t>(result->reduced_problem);
858857
presolve_time = lp_timer.elapsed_time();
859858
CUOPT_LOG_INFO("Papilo presolve time: %f", presolve_time);
860859
}

cpp/src/mip/presolve/third_party_presolve.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ void set_presolve_parameters(papilo::Presolve<f_t>& presolver,
391391
}
392392

393393
template <typename i_t, typename f_t>
394-
std::pair<optimization_problem_t<i_t, f_t>, bool> third_party_presolve_t<i_t, f_t>::apply(
394+
std::optional<third_party_presolve_result_t<i_t, f_t>> third_party_presolve_t<i_t, f_t>::apply(
395395
optimization_problem_t<i_t, f_t> const& op_problem,
396396
problem_category_t category,
397397
bool dual_postsolve,
@@ -423,7 +423,7 @@ std::pair<optimization_problem_t<i_t, f_t>, bool> third_party_presolve_t<i_t, f_
423423
check_presolve_status(result.status);
424424
if (result.status == papilo::PresolveStatus::kInfeasible ||
425425
result.status == papilo::PresolveStatus::kUnbndOrInfeas) {
426-
return std::make_pair(optimization_problem_t<i_t, f_t>(op_problem.get_handle_ptr()), false);
426+
return std::nullopt;
427427
}
428428
post_solve_storage_ = result.postsolve;
429429
CUOPT_LOG_INFO("Presolve removed: %d constraints, %d variables, %d nonzeros",
@@ -435,8 +435,16 @@ std::pair<optimization_problem_t<i_t, f_t>, bool> third_party_presolve_t<i_t, f_
435435
papilo_problem.getNCols(),
436436
papilo_problem.getConstraintMatrix().getNnz());
437437

438-
return std::make_pair(
439-
build_optimization_problem<i_t, f_t>(papilo_problem, op_problem.get_handle_ptr()), true);
438+
auto opt_problem =
439+
build_optimization_problem<i_t, f_t>(papilo_problem, op_problem.get_handle_ptr());
440+
auto col_flags = papilo_problem.getColFlags();
441+
std::vector<i_t> implied_integer_indices;
442+
for (size_t i = 0; i < col_flags.size(); i++) {
443+
if (col_flags[i].test(papilo::ColFlag::kImplInt)) implied_integer_indices.push_back(i);
444+
}
445+
446+
return std::make_optional(
447+
third_party_presolve_result_t<i_t, f_t>{opt_problem, implied_integer_indices});
440448
}
441449

442450
template <typename i_t, typename f_t>

cpp/src/mip/presolve/third_party_presolve.hpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,25 @@
1717

1818
#pragma once
1919

20+
#include <optional>
21+
2022
#include <cuopt/linear_programming/optimization_problem.hpp>
2123

2224
namespace cuopt::linear_programming::detail {
2325

26+
template <typename i_t, typename f_t>
27+
struct third_party_presolve_result_t {
28+
optimization_problem_t<i_t, f_t> reduced_problem;
29+
std::vector<i_t> implied_integer_indices;
30+
// clique info, etc...
31+
};
32+
2433
template <typename i_t, typename f_t>
2534
class third_party_presolve_t {
2635
public:
2736
third_party_presolve_t() = default;
2837

29-
std::pair<optimization_problem_t<i_t, f_t>, bool> apply(
38+
std::optional<third_party_presolve_result_t<i_t, f_t>> apply(
3039
optimization_problem_t<i_t, f_t> const& op_problem,
3140
problem_category_t category,
3241
bool dual_postsolve,

cpp/src/mip/problem/presolve_data.cuh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class presolve_data_t {
3939
objective_offset(problem.get_objective_offset()),
4040
objective_scaling_factor(problem.get_objective_scaling_factor()),
4141
variable_mapping(0, stream),
42-
fixed_var_assignment(0, stream)
42+
fixed_var_assignment(0, stream),
43+
var_flags(0, stream)
4344
{
4445
}
4546

@@ -50,7 +51,8 @@ class presolve_data_t {
5051
objective_offset(other.objective_offset),
5152
objective_scaling_factor(other.objective_scaling_factor),
5253
variable_mapping(other.variable_mapping, stream),
53-
fixed_var_assignment(other.fixed_var_assignment, stream)
54+
fixed_var_assignment(other.fixed_var_assignment, stream),
55+
var_flags(other.var_flags, stream)
5456
{
5557
}
5658

@@ -86,6 +88,7 @@ class presolve_data_t {
8688

8789
rmm::device_uvector<i_t> variable_mapping;
8890
rmm::device_uvector<f_t> fixed_var_assignment;
91+
rmm::device_uvector<i_t> var_flags;
8992
};
9093

9194
} // namespace linear_programming::detail

cpp/src/mip/problem/problem.cu

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ void problem_t<i_t, f_t>::op_problem_cstr_body(const optimization_problem_t<i_t,
8989
// If maximization problem, convert the problem
9090
if (maximize) convert_to_maximization_problem(*this);
9191
if (is_mip) {
92+
presolve_data.var_flags.resize(n_variables, handle_ptr->get_stream());
93+
thrust::fill(handle_ptr->get_thrust_policy(),
94+
presolve_data.var_flags.begin(),
95+
presolve_data.var_flags.end(),
96+
0);
9297
integer_indices.resize(n_variables, handle_ptr->get_stream());
9398
is_binary_variable.resize(n_variables, handle_ptr->get_stream());
9499
compute_n_integer_vars();
@@ -190,6 +195,7 @@ problem_t<i_t, f_t>::problem_t(const problem_t<i_t, f_t>& problem_)
190195
objective_name(problem_.objective_name),
191196
is_scaled_(problem_.is_scaled_),
192197
preprocess_called(problem_.preprocess_called),
198+
objective_is_integral(problem_.objective_is_integral),
193199
lp_state(problem_.lp_state),
194200
fixing_helpers(problem_.fixing_helpers, handle_ptr),
195201
vars_with_objective_coeffs(problem_.vars_with_objective_coeffs),
@@ -284,6 +290,7 @@ problem_t<i_t, f_t>::problem_t(const problem_t<i_t, f_t>& problem_, bool no_deep
284290
objective_name(problem_.objective_name),
285291
is_scaled_(problem_.is_scaled_),
286292
preprocess_called(problem_.preprocess_called),
293+
objective_is_integral(problem_.objective_is_integral),
287294
lp_state(problem_.lp_state),
288295
fixing_helpers(problem_.fixing_helpers, handle_ptr),
289296
vars_with_objective_coeffs(problem_.vars_with_objective_coeffs),
@@ -933,6 +940,8 @@ typename problem_t<i_t, f_t>::view_t problem_t<i_t, f_t>::view()
933940
v.variable_types = raft::device_span<var_t>{variable_types.data(), variable_types.size()};
934941
v.is_binary_variable =
935942
raft::device_span<i_t>{is_binary_variable.data(), is_binary_variable.size()};
943+
v.var_flags =
944+
raft::device_span<i_t>{presolve_data.var_flags.data(), presolve_data.var_flags.size()};
936945
v.related_variables = raft::device_span<i_t>{related_variables.data(), related_variables.size()};
937946
v.related_variables_offsets =
938947
raft::device_span<i_t>{related_variables_offsets.data(), related_variables_offsets.size()};
@@ -953,6 +962,7 @@ void problem_t<i_t, f_t>::resize_variables(size_t size)
953962
variable_types.resize(size, handle_ptr->get_stream());
954963
objective_coefficients.resize(size, handle_ptr->get_stream());
955964
is_binary_variable.resize(size, handle_ptr->get_stream());
965+
presolve_data.var_flags.resize(size, handle_ptr->get_stream()); // 0 is default - no flag
956966
related_variables_offsets.resize(size, handle_ptr->get_stream());
957967
}
958968
@@ -1060,6 +1070,32 @@ void problem_t<i_t, f_t>::insert_constraints(constraints_delta_t<i_t, f_t>& h_co
10601070
combine_constraint_bounds<i_t, f_t>(*this, combined_bounds);
10611071
}
10621072
1073+
template <typename i_t, typename f_t>
1074+
void problem_t<i_t, f_t>::set_implied_integers(const std::vector<i_t>& implied_integer_indices)
1075+
{
1076+
raft::common::nvtx::range fun_scope("set_implied_integers");
1077+
auto d_indices = cuopt::device_copy(implied_integer_indices, handle_ptr->get_stream());
1078+
print("implied integer indices", d_indices);
1079+
thrust::for_each(handle_ptr->get_thrust_policy(),
1080+
d_indices.begin(),
1081+
d_indices.end(),
1082+
[var_flags = make_span(presolve_data.var_flags),
1083+
var_types = make_span(variable_types)] __device__(i_t idx) {
1084+
cuopt_assert(idx < var_flags.size(), "Index out of bounds");
1085+
cuopt_assert(var_types[idx] == var_t::CONTINUOUS, "Variable is integer");
1086+
var_flags[idx] |= (i_t)VAR_IMPLIED_INTEGER;
1087+
});
1088+
objective_is_integral = thrust::all_of(handle_ptr->get_thrust_policy(),
1089+
thrust::make_counting_iterator(0),
1090+
thrust::make_counting_iterator(n_variables),
1091+
[v = view()] __device__(i_t var_idx) {
1092+
if (v.objective_coefficients[var_idx] == 0) return true;
1093+
return v.is_integer(v.objective_coefficients[var_idx]) &&
1094+
(v.variable_types[var_idx] == var_t::INTEGER ||
1095+
(v.var_flags[var_idx] & VAR_IMPLIED_INTEGER));
1096+
});
1097+
}
1098+
10631099
template <typename i_t, typename f_t>
10641100
void problem_t<i_t, f_t>::fix_given_variables(problem_t<i_t, f_t>& original_problem,
10651101
rmm::device_uvector<f_t>& assignment,
@@ -1249,6 +1285,13 @@ void problem_t<i_t, f_t>::remove_given_variables(problem_t<i_t, f_t>& original_p
12491285
original_problem.variable_types.begin(),
12501286
variable_types.begin());
12511287
variable_types.resize(variable_map.size(), handle_ptr->get_stream());
1288+
// keep implied-integer and other flags consistent with new variable set
1289+
thrust::gather(handle_ptr->get_thrust_policy(),
1290+
variable_map.begin(),
1291+
variable_map.end(),
1292+
original_problem.presolve_data.var_flags.begin(),
1293+
presolve_data.var_flags.begin());
1294+
presolve_data.var_flags.resize(variable_map.size(), handle_ptr->get_stream());
12521295
const i_t TPB = 64;
12531296
// compute new offsets
12541297
compute_new_offsets<i_t, f_t><<<variable_map.size(), TPB, 0, handle_ptr->get_stream()>>>(

cpp/src/mip/problem/problem.cuh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class problem_t {
9191

9292
void insert_variables(variables_delta_t<i_t, f_t>& h_vars);
9393
void insert_constraints(constraints_delta_t<i_t, f_t>& h_constraints);
94+
void set_implied_integers(const std::vector<i_t>& implied_integer_indices);
9495
void resize_variables(size_t size);
9596
void resize_constraints(size_t matrix_size, size_t constraint_size, size_t var_size);
9697
void preprocess_problem();
@@ -100,6 +101,7 @@ class problem_t {
100101
void post_process_solution(solution_t<i_t, f_t>& solution);
101102
void compute_transpose_of_problem();
102103
f_t get_user_obj_from_solver_obj(f_t solver_obj) const;
104+
bool is_objective_integral() const { return objective_is_integral; }
103105
void compute_integer_fixed_problem();
104106
void fill_integer_fixed_problem(rmm::device_uvector<f_t>& assignment,
105107
const raft::handle_t* handle_ptr);
@@ -116,6 +118,10 @@ class problem_t {
116118
void compute_vars_with_objective_coeffs();
117119
void test_problem_fixing_time();
118120

121+
enum var_flags_t : i_t {
122+
VAR_IMPLIED_INTEGER = 1 << 0,
123+
};
124+
119125
struct view_t {
120126
HDI std::pair<i_t, i_t> reverse_range_for_var(i_t v) const
121127
{
@@ -193,6 +199,7 @@ class problem_t {
193199
raft::device_span<f_t> constraint_upper_bounds;
194200
raft::device_span<var_t> variable_types;
195201
raft::device_span<i_t> is_binary_variable;
202+
raft::device_span<i_t> var_flags;
196203
raft::device_span<i_t> integer_indices;
197204
raft::device_span<i_t> binary_indices;
198205
raft::device_span<i_t> nonbinary_indices;
@@ -269,6 +276,7 @@ class problem_t {
269276
f_t objective_offset;
270277
bool is_scaled_{false};
271278
bool preprocess_called{false};
279+
bool objective_is_integral{false};
272280
// this LP state keeps the warm start data of some solution of
273281
// 1. Original problem: it is unchanged and part of it is used
274282
// to warm start slightly modified problems.

cpp/src/mip/solve.cu

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -209,23 +209,27 @@ mip_solution_t<i_t, f_t> solve_mip(optimization_problem_t<i_t, f_t>& op_problem,
209209
// Note that this is not the presolve time, but the time limit for presolve.
210210
const double presolve_time_limit = std::min(0.1 * time_limit, 60.0);
211211
const bool dual_postsolve = false;
212-
presolver = std::make_unique<detail::third_party_presolve_t<i_t, f_t>>();
213-
auto [reduced_op_problem, feasible] =
214-
presolver->apply(op_problem,
215-
cuopt::linear_programming::problem_category_t::MIP,
216-
dual_postsolve,
217-
settings.tolerances.absolute_tolerance,
218-
settings.tolerances.relative_tolerance,
219-
presolve_time_limit,
220-
settings.num_cpu_threads);
221-
if (!feasible) {
212+
presolver = std::make_unique<detail::third_party_presolve_t<i_t, f_t>>();
213+
auto result = presolver->apply(op_problem,
214+
cuopt::linear_programming::problem_category_t::MIP,
215+
dual_postsolve,
216+
settings.tolerances.absolute_tolerance,
217+
settings.tolerances.relative_tolerance,
218+
presolve_time_limit,
219+
settings.num_cpu_threads);
220+
if (!result.has_value()) {
222221
return mip_solution_t<i_t, f_t>(mip_termination_status_t::Infeasible,
223222
solver_stats_t<i_t, f_t>{},
224223
op_problem.get_handle_ptr()->get_stream());
225224
}
226225

227-
problem = detail::problem_t<i_t, f_t>(reduced_op_problem);
226+
problem = detail::problem_t<i_t, f_t>(result->reduced_problem);
227+
problem.set_implied_integers(result->implied_integer_indices);
228228
presolve_time = timer.elapsed_time();
229+
if (result->implied_integer_indices.size() > 0) {
230+
CUOPT_LOG_INFO("%d implied integers", result->implied_integer_indices.size());
231+
}
232+
if (problem.is_objective_integral()) { CUOPT_LOG_INFO("Objective function is integral"); }
229233
CUOPT_LOG_INFO("Papilo presolve time: %f", presolve_time);
230234
}
231235
if (settings.user_problem_file != "") {

cpp/tests/mip/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ ConfigureTest(UNIT_TEST
4343
ConfigureTest(EMPTY_FIXED_PROBLEMS_TEST
4444
${CMAKE_CURRENT_SOURCE_DIR}/empty_fixed_problems_test.cu
4545
)
46+
ConfigureTest(PRESOLVE_TEST
47+
${CMAKE_CURRENT_SOURCE_DIR}/presolve_test.cu
48+
)
4649
# Disable for now
4750
# ConfigureTest(FEASIBILITY_JUMP_TEST
4851
# ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump_tests.cu

cpp/tests/mip/presolve_test.cu

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights
3+
* reserved. SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#include "../linear_programming/utilities/pdlp_test_utilities.cuh"
19+
20+
#include <cuopt/linear_programming/solve.hpp>
21+
#include <linear_programming/utils.cuh>
22+
#include <mip/presolve/third_party_presolve.hpp>
23+
#include <mip/problem/problem.cuh>
24+
#include <mps_parser/mps_data_model.hpp>
25+
#include <mps_parser/parser.hpp>
26+
#include <utilities/common_utils.hpp>
27+
#include <utilities/copy_helpers.hpp>
28+
#include <utilities/error.hpp>
29+
30+
#include <raft/core/handle.hpp>
31+
#include <raft/util/cudart_utils.hpp>
32+
33+
#include <gtest/gtest.h>
34+
35+
#include <cstdint>
36+
#include <sstream>
37+
#include <string>
38+
#include <vector>
39+
40+
namespace cuopt::linear_programming::test {
41+
42+
TEST(problem, find_implied_integers)
43+
{
44+
const raft::handle_t handle_{};
45+
46+
auto path = make_path_absolute("mip/fiball.mps");
47+
auto mps_data_model = cuopt::mps_parser::parse_mps<int, double>(path, false);
48+
auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_data_model);
49+
auto presolver = std::make_unique<detail::third_party_presolve_t<int, double>>();
50+
auto result = presolver->apply(
51+
op_problem, cuopt::linear_programming::problem_category_t::MIP, false, 1e-6, 1e-12, 20, 1);
52+
ASSERT_TRUE(result.has_value());
53+
54+
auto problem = detail::problem_t<int, double>(result->reduced_problem);
55+
problem.set_implied_integers(result->implied_integer_indices);
56+
ASSERT_TRUE(result->implied_integer_indices.size() > 0);
57+
auto var_types = host_copy(problem.variable_types);
58+
// Find the index of the one continuous variable
59+
auto it = std::find_if(var_types.begin(), var_types.end(), [](var_t var_type) {
60+
return var_type == var_t::CONTINUOUS;
61+
});
62+
ASSERT_NE(it, var_types.end());
63+
ASSERT_EQ(problem.presolve_data.var_flags.size(), var_types.size());
64+
// Ensure it is an implied integer
65+
EXPECT_EQ(problem.presolve_data.var_flags.element(it - var_types.begin(), handle_.get_stream()),
66+
((int)detail::problem_t<int, double>::var_flags_t::VAR_IMPLIED_INTEGER));
67+
}
68+
69+
} // namespace cuopt::linear_programming::test

0 commit comments

Comments
 (0)