From 16fe2c66f30a436c7b4d28226ffb1fb20c3c51b0 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 17 Jun 2025 12:52:07 +0000 Subject: [PATCH] empty obj bugfix --- cpp/src/mip/problem/problem.cu | 27 ++++++++++++------- cpp/src/mip/problem/problem_helpers.cuh | 1 + cpp/src/mip/solve.cu | 9 +++++++ cpp/tests/mip/empty_fixed_problems_test.cu | 14 ++++++++++ cpp/tests/mip/termination_test.cu | 7 +++++ .../mip/empty-max-problem-objective-vars.mps | 17 ++++++++++++ datasets/mip/empty-problem-objective-vars.mps | 12 +++++++++ datasets/mip/trivial-presolve-no-obj-vars.mps | 14 ++++++++++ 8 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 datasets/mip/empty-max-problem-objective-vars.mps create mode 100644 datasets/mip/empty-problem-objective-vars.mps create mode 100644 datasets/mip/trivial-presolve-no-obj-vars.mps diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 5b8511b92..d5be3ce7a 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -58,12 +58,6 @@ void problem_t::op_problem_cstr_body(const optimization_problem_t::op_problem_cstr_body(const optimization_problem_tget_problem_category() != problem_category_t::LP; if (is_mip) { // Resize what is needed for MIP @@ -90,6 +85,7 @@ void problem_t::op_problem_cstr_body(const optimization_problem_t::compute_transpose_of_problem() reverse_offsets.resize(n_variables + 1, handle_ptr->get_stream()); reverse_constraints.resize(nnz, handle_ptr->get_stream()); reverse_coefficients.resize(nnz, handle_ptr->get_stream()); + + // Special case if A is empty + // as cuSparse had a bug up until 12.9 causing cusparseCsr2cscEx2 to return incorrect results + // for empty matrices (CUSPARSE-2319) + // In this case, construct it manually + if (reverse_coefficients.is_empty()) { + thrust::fill( + handle_ptr->get_thrust_policy(), reverse_offsets.begin(), reverse_offsets.end(), 0); + return; + } + raft::sparse::linalg::csr_transpose(*handle_ptr, offsets.data(), variables.data(), @@ -318,12 +325,12 @@ void problem_t::check_problem_representation(bool check_transposed, { raft::common::nvtx::range scope("check_problem_representation"); - // Presolve reductions might trivially solve the problem to optimality/infeasibility. - // In this case, it is exptected that the problem fields are empty. cuopt_assert(!offsets.is_empty(), "A_offsets must never be empty."); if (check_transposed) { cuopt_assert(!reverse_offsets.is_empty(), "A_offsets must never be empty."); } + // Presolve reductions might trivially solve the problem to optimality/infeasibility. + // In this case, it is exptected that the problem fields are empty. if (!empty) { // Check for empty fields cuopt_assert(!coefficients.is_empty(), "A_values must be set before calling the solver."); @@ -334,8 +341,9 @@ void problem_t::check_problem_representation(bool check_transposed, cuopt_assert(!reverse_constraints.is_empty(), "A_indices must be set before calling the solver."); } - cuopt_assert(!objective_coefficients.is_empty(), "c must be set before calling the solver."); } + cuopt_assert(objective_coefficients.size() == n_variables, + "objective_coefficients size mismatch"); // Check CSR validity check_csr_representation( @@ -697,6 +705,7 @@ void problem_t::recompute_auxilliary_data(bool check_representation) template void problem_t::compute_n_integer_vars() { + cuopt_assert(n_variables == variable_types.size(), "size mismatch"); integer_indices.resize(n_variables, handle_ptr->get_stream()); auto end = thrust::copy_if(handle_ptr->get_thrust_policy(), diff --git a/cpp/src/mip/problem/problem_helpers.cuh b/cpp/src/mip/problem/problem_helpers.cuh index ddb9cbe7e..2104f7eaf 100644 --- a/cpp/src/mip/problem/problem_helpers.cuh +++ b/cpp/src/mip/problem/problem_helpers.cuh @@ -138,6 +138,7 @@ static void convert_to_maximization_problem(detail::problem_t& op_prob // negating objective coeffs op_problem.presolve_data.objective_scaling_factor = -op_problem.presolve_data.objective_scaling_factor; + op_problem.presolve_data.objective_offset = -op_problem.presolve_data.objective_offset; } /* diff --git a/cpp/src/mip/solve.cu b/cpp/src/mip/solve.cu index e96456801..b468a07e9 100644 --- a/cpp/src/mip/solve.cu +++ b/cpp/src/mip/solve.cu @@ -83,6 +83,15 @@ mip_solution_t run_mip(detail::problem_t& problem, // if the input problem is empty: early exit if (problem.empty) { detail::solution_t solution(problem); + problem.preprocess_problem(); + thrust::for_each(problem.handle_ptr->get_thrust_policy(), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(problem.n_variables), + [sol = solution.assignment.data(), pb = problem.view()] __device__(i_t index) { + sol[index] = pb.objective_coefficients[index] > 0 + ? pb.variable_lower_bounds[index] + : pb.variable_upper_bounds[index]; + }); problem.post_process_solution(solution); solution.compute_objective(); // just to ensure h_user_obj is set auto stats = solver_stats_t{}; diff --git a/cpp/tests/mip/empty_fixed_problems_test.cu b/cpp/tests/mip/empty_fixed_problems_test.cu index 56f700c55..c93cde575 100644 --- a/cpp/tests/mip/empty_fixed_problems_test.cu +++ b/cpp/tests/mip/empty_fixed_problems_test.cu @@ -79,4 +79,18 @@ TEST(mip_solve, empty_problem_test) EXPECT_NEAR(obj_val, 81, 1e-5); } +TEST(mip_solve, empty_problem_with_objective_test) +{ + auto [termination_status, obj_val] = test_mps_file("mip/empty-problem-objective-vars.mps"); + EXPECT_EQ(termination_status, mip_termination_status_t::Optimal); + EXPECT_NEAR(obj_val, -2, 1e-5); +} + +TEST(mip_solve, empty_max_problem_with_objective_test) +{ + auto [termination_status, obj_val] = test_mps_file("mip/empty-max-problem-objective-vars.mps"); + EXPECT_EQ(termination_status, mip_termination_status_t::Optimal); + EXPECT_NEAR(obj_val, 11, 1e-5); +} + } // namespace cuopt::linear_programming::test diff --git a/cpp/tests/mip/termination_test.cu b/cpp/tests/mip/termination_test.cu index 4ed7a6004..046bd92fb 100644 --- a/cpp/tests/mip/termination_test.cu +++ b/cpp/tests/mip/termination_test.cu @@ -70,6 +70,13 @@ TEST(termination_status, trivial_presolve_optimality_test) EXPECT_EQ(obj_val, -1); } +TEST(termination_status, trivial_presolve_no_obj_vars_test) +{ + auto [termination_status, obj_val, lb] = test_mps_file("mip/trivial-presolve-no-obj-vars.mps"); + EXPECT_EQ(termination_status, mip_termination_status_t::Optimal); + EXPECT_EQ(obj_val, 0); +} + TEST(termination_status, presolve_optimality_test) { auto [termination_status, obj_val, lb] = test_mps_file("mip/sudoku.mps"); diff --git a/datasets/mip/empty-max-problem-objective-vars.mps b/datasets/mip/empty-max-problem-objective-vars.mps new file mode 100644 index 000000000..54459647b --- /dev/null +++ b/datasets/mip/empty-max-problem-objective-vars.mps @@ -0,0 +1,17 @@ +NAME +OBJSENSE + MAX +ROWS + N OBJ +COLUMNS + MARKER 'MARKER' 'INTORG' + x1 OBJ -2 + x2 OBJ 5.5 + MARKER 'MARKER' 'INTEND' +RHS +RANGES +BOUNDS + BV bounds x1 + LI bounds x2 -2 + UI bounds x2 2 +ENDATA diff --git a/datasets/mip/empty-problem-objective-vars.mps b/datasets/mip/empty-problem-objective-vars.mps new file mode 100644 index 000000000..d661702d6 --- /dev/null +++ b/datasets/mip/empty-problem-objective-vars.mps @@ -0,0 +1,12 @@ +NAME +ROWS + N OBJ +COLUMNS + MARKER 'MARKER' 'INTORG' + x1 OBJ -2 + MARKER 'MARKER' 'INTEND' +RHS +RANGES +BOUNDS + BV bounds x1 +ENDATA diff --git a/datasets/mip/trivial-presolve-no-obj-vars.mps b/datasets/mip/trivial-presolve-no-obj-vars.mps new file mode 100644 index 000000000..80cc352f1 --- /dev/null +++ b/datasets/mip/trivial-presolve-no-obj-vars.mps @@ -0,0 +1,14 @@ +NAME EXAMPLE +ROWS + N OBJ + E C1 +COLUMNS + X1 C1 1 +RHS + RHS1 C1 0 +BOUNDS + LO BND1 X1 0 + UP BND1 X1 1 + LO BND1 X2 0 + UP BND1 X2 1 +ENDATA