Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
67f293b
tmp
aliceb-nv Aug 28, 2025
ef26750
added tests
aliceb-nv Aug 29, 2025
480c4f1
use GCC 14, consolidate dependency groups, update pre-commit hooks (#…
jameslamb Aug 29, 2025
9154170
fix failing test
aliceb-nv Sep 1, 2025
81c8553
Revert "use GCC 14, consolidate dependency groups, update pre-commit …
aliceb-nv Sep 1, 2025
d614f64
remove undeeded change
aliceb-nv Sep 1, 2025
b438170
Merge branch 'branch-25.10' into invalid-bounds-fix
aliceb-nv Sep 1, 2025
aa58f62
Add Commit Sha to container for reference (#362)
rgsl888prabhu Sep 2, 2025
2eccc32
QPS extension for MPS (#352)
Franc-Z Sep 3, 2025
1e71658
Faster engine compile time (#316)
Kh4ster Sep 3, 2025
f13ea6f
Fix out-of-bound access in `clean_up_infeasibilities`. (#346)
legrosbuffle Sep 3, 2025
c2b34c8
Decompression for .mps.gz and .mps.bz2 files (#357)
ahehn-nv Sep 3, 2025
90095df
Add documentation on nightly installation commands (#367)
rgsl888prabhu Sep 3, 2025
5478578
Warn in case a dependent library is not found in libcuopt load (#375)
rgsl888prabhu Sep 5, 2025
d4a4e5e
Print cuOpt version / machine info before solving (#370)
aliceb-nv Sep 5, 2025
29fae5f
Enable parallelism for root node presolve (#371)
hlinsen Sep 5, 2025
bbdf0e6
Build and test with CUDA 13.0.0 (#366)
jameslamb Sep 5, 2025
738f43c
CUDA 13 support: follow-ups (#377)
jameslamb Sep 6, 2025
db1b630
Adding support nightly cuopt-examples notebook testing (#342)
rgsl888prabhu Sep 8, 2025
e07f9a7
Add support for cuda13 container and fix cuda13 lib issues in wheel (…
rgsl888prabhu Sep 9, 2025
a1a15d2
Combined variable bounds (#372)
kaatish Sep 10, 2025
7a9e7bf
[FIX] Fix high GPU memory usage (#351)
aliceb-nv Sep 10, 2025
75f5261
Implement node presolve (#368)
rg20 Sep 10, 2025
f37631f
Loosen presolve tolerance and update timers to report cumulative pres…
hlinsen Sep 15, 2025
dfe4966
Doc update for container version update and add nvidia-cuda-runtime a…
rgsl888prabhu Sep 15, 2025
49177a4
Add video link to the docs and Readme (#393)
rgsl888prabhu Sep 15, 2025
8d9587e
Add name to drop down for video link (#396)
rgsl888prabhu Sep 16, 2025
4609fbd
Add read/write MPS and relaxation to python API (#323)
Iroy30 Sep 17, 2025
3d4a42c
Remove limiting_resource_adaptor leftover (#398)
aliceb-nv Sep 17, 2025
5ce1e14
Add sanitizer build option (#385)
akifcorduk Sep 17, 2025
c80d730
Heuristic Improvements: balance between generation and improvement he…
akifcorduk Sep 18, 2025
e3f90c5
Fix bug in fixed_problem_computation (#403)
akifcorduk Sep 19, 2025
ae659b9
Simple diving for Branch-and-Bound (#305)
nguidotti Sep 19, 2025
ac08021
amend
aliceb-nv Sep 22, 2025
98781b1
merge
aliceb-nv Sep 22, 2025
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
9 changes: 6 additions & 3 deletions cpp/libmps_parser/src/mps_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -784,9 +784,12 @@ void mps_parser_t<i_t, f_t>::parse_string(char* buf)
variable_lower_bounds[i] = 0;
variable_upper_bounds[i] = 1;
}
mps_parser_expects(variable_lower_bounds[i] <= variable_upper_bounds[i],
error_type_t::ValidationError,
"MPS Parser Internal Error - Please contact cuOpt team");
if (variable_lower_bounds[i] > variable_upper_bounds[i]) {
printf("WARNING: Variable %d has crossing bounds: %f > %f\n",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is logger not suitable here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't link against rapids_logger in libmps_parser iirc

i,
variable_lower_bounds[i],
variable_upper_bounds[i]);
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions cpp/src/linear_programming/solve.cu
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ optimization_problem_solution_t<i_t, f_t> solve_lp(optimization_problem_t<i_t, f
problem_checking_t<i_t, f_t>::check_initial_solution_representation(op_problem, settings);
}

// Check for crossing bounds. Return infeasible if there are any
if (problem_checking_t<i_t, f_t>::has_crossing_bounds(op_problem)) {
return optimization_problem_solution_t<i_t, f_t>(pdlp_termination_status_t::PrimalInfeasible,
op_problem.get_handle_ptr()->get_stream());
}

auto lp_timer = cuopt::timer_t(settings.time_limit);
detail::problem_t<i_t, f_t> problem(op_problem);

Expand Down
30 changes: 30 additions & 0 deletions cpp/src/linear_programming/utilities/problem_checking.cu
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include <mip/mip_constants.hpp>
#include <utilities/copy_helpers.hpp>

#include <mip/problem/problem.cuh>

#include <thrust/functional.h>
#include <thrust/logical.h>
#include <thrust/sort.h>
Expand Down Expand Up @@ -320,6 +322,34 @@ void problem_checking_t<i_t, f_t>::check_unscaled_solution(
}
}

template <typename i_t, typename f_t>
bool problem_checking_t<i_t, f_t>::has_crossing_bounds(
const optimization_problem_t<i_t, f_t>& op_problem)
{
// Check if all variable bounds are valid (upper >= lower)
bool all_variable_bounds_valid = thrust::all_of(
op_problem.get_handle_ptr()->get_thrust_policy(),
thrust::make_counting_iterator(0),
thrust::make_counting_iterator(0) + op_problem.get_variable_upper_bounds().size(),
[upper_bounds = make_span(op_problem.get_variable_upper_bounds()),
lower_bounds = make_span(op_problem.get_variable_lower_bounds())] __device__(size_t i) {
return upper_bounds[i] >= lower_bounds[i];
});

// Check if all constraint bounds are valid (upper >= lower)
bool all_constraint_bounds_valid = thrust::all_of(
op_problem.get_handle_ptr()->get_thrust_policy(),
thrust::make_counting_iterator(0),
thrust::make_counting_iterator(0) + op_problem.get_constraint_upper_bounds().size(),
[upper_bounds = make_span(op_problem.get_constraint_upper_bounds()),
lower_bounds = make_span(op_problem.get_constraint_lower_bounds())] __device__(size_t i) {
return upper_bounds[i] >= lower_bounds[i];
});

// Return true if any bounds are invalid (crossing)
return !all_variable_bounds_valid || !all_constraint_bounds_valid;
}

#define INSTANTIATE(F_TYPE) template class problem_checking_t<int, F_TYPE>;

#if MIP_INSTANTIATE_FLOAT
Expand Down
11 changes: 10 additions & 1 deletion cpp/src/linear_programming/utilities/problem_checking.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@
#include <cuopt/linear_programming/optimization_problem.hpp>
#include <cuopt/linear_programming/pdlp/solver_settings.hpp>

#include <mip/problem/problem.cuh>
namespace rmm {
template <typename T>
class device_uvector;
} // namespace rmm

namespace cuopt::linear_programming {

namespace detail {
template <typename i_t, typename f_t>
class problem_t;
} // namespace detail

template <typename i_t, typename f_t>
class problem_checking_t {
public:
static void check_csr_representation(const optimization_problem_t<i_t, f_t>& op_problem);
// Check all fields and convert row_types to constraints lower/upper bounds if needed
static void check_problem_representation(const optimization_problem_t<i_t, f_t>& op_problem);
static bool has_crossing_bounds(const optimization_problem_t<i_t, f_t>& op_problem);

static void check_scaled_problem(detail::problem_t<i_t, f_t> const& scaled_problem,
detail::problem_t<i_t, f_t> const& op_problem);
Expand Down
11 changes: 1 addition & 10 deletions cpp/src/mip/presolve/third_party_presolve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
namespace cuopt::linear_programming::detail {

static papilo::PostsolveStorage<double> post_solve_storage_;
static int presolve_calls_ = 0;
static bool maximize_ = false;
static bool maximize_ = false;

template <typename i_t, typename f_t>
papilo::Problem<f_t> build_papilo_problem(const optimization_problem_t<i_t, f_t>& op_problem)
Expand Down Expand Up @@ -356,10 +355,6 @@ std::pair<optimization_problem_t<i_t, f_t>, bool> third_party_presolve_t<i_t, f_
double time_limit,
i_t num_cpu_threads)
{
cuopt_expects(
presolve_calls_ == 0, error_type_t::ValidationError, "Presolve can only be called once");
presolve_calls_++;

papilo::Problem<f_t> papilo_problem = build_papilo_problem(op_problem);

CUOPT_LOG_INFO("Unpresolved problem:: %d constraints, %d variables, %d nonzeros",
Expand All @@ -379,7 +374,6 @@ std::pair<optimization_problem_t<i_t, f_t>, bool> third_party_presolve_t<i_t, f_
check_presolve_status(result.status);
if (result.status == papilo::PresolveStatus::kInfeasible ||
result.status == papilo::PresolveStatus::kUnbndOrInfeas) {
--presolve_calls_;
return std::make_pair(optimization_problem_t<i_t, f_t>(op_problem.get_handle_ptr()), false);
}
post_solve_storage_ = result.postsolve;
Expand All @@ -404,9 +398,6 @@ void third_party_presolve_t<i_t, f_t>::undo(rmm::device_uvector<f_t>& primal_sol
bool status_to_skip,
rmm::cuda_stream_view stream_view)
{
--presolve_calls_;
cuopt_expects(
presolve_calls_ == 0, error_type_t::ValidationError, "Postsolve can only be called once");
if (status_to_skip) { return; }
std::vector<f_t> primal_sol_vec_h(primal_solution.size());
raft::copy(primal_sol_vec_h.data(), primal_solution.data(), primal_solution.size(), stream_view);
Expand Down
7 changes: 7 additions & 0 deletions cpp/src/mip/solve.cu
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ mip_solution_t<i_t, f_t> solve_mip(optimization_problem_t<i_t, f_t>& op_problem,
problem_checking_t<i_t, f_t>::check_problem_representation(op_problem);
problem_checking_t<i_t, f_t>::check_initial_solution_representation(op_problem, settings);

// Check for crossing bounds. Return infeasible if there are any
if (problem_checking_t<i_t, f_t>::has_crossing_bounds(op_problem)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There may be some unset vars which we set in problem constructor. I think this should come after the problem cosntructor.

Copy link
Contributor

Choose a reason for hiding this comment

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

We have to do this for LP as well.

return mip_solution_t<i_t, f_t>(mip_termination_status_t::Infeasible,
solver_stats_t<i_t, f_t>{},
op_problem.get_handle_ptr()->get_stream());
}

auto timer = cuopt::timer_t(time_limit);

double presolve_time = 0.0;
Expand Down
161 changes: 161 additions & 0 deletions cpp/tests/linear_programming/c_api_tests/c_api_test.c
Copy link
Contributor

@nguidotti nguidotti Sep 9, 2025

Choose a reason for hiding this comment

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

A small suggestion: instead of goto, is not better to transform

Done:

  cuOptDestroyProblem(&problem);
  cuOptDestroySolverSettings(&settings);
  cuOptDestroySolution(&solution);

into a separated routine (e.g., cleanup) and then call cleanup + return to end the routine?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That part of the code wasn't written by me, but to be fair it's more of a flavour / personal prerefence kind of thing :) It is a common pattern in C for resource cleanup; and it lets the code have a single exit point

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough

Original file line number Diff line number Diff line change
Expand Up @@ -872,3 +872,164 @@ cuopt_int_t test_ranged_problem(cuopt_int_t *termination_status_ptr, cuopt_float

return status;
}

// Test invalid bounds scenario (what MOI wrapper was producing)
cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip)
{
cuOptOptimizationProblem problem = NULL;
cuOptSolverSettings settings = NULL;
cuOptSolution solution = NULL;

/* Test the invalid bounds scenario:
maximize 2*x
subject to:
x >= 0.2
x <= 0.5
x is binary (0 or 1)

After MOI wrapper processing:
- Lower bound = ceil(max(0.0, 0.2)) = 1.0
- Upper bound = floor(min(1.0, 0.5)) = 0.0
- Result: 1.0 <= x <= 0.0 (INVALID!)
*/

cuopt_int_t num_variables = 1;
cuopt_int_t num_constraints = 2;
cuopt_int_t nnz = 2;

// CSR format constraint matrix
// From the constraints:
// x >= 0.2
// x <= 0.5
cuopt_int_t row_offsets[] = {0, 1, 2};
cuopt_int_t column_indices[] = {0, 0};
cuopt_float_t values[] = {1.0, 1.0};

// Objective coefficients
// From the objective function: maximize 2*x
cuopt_float_t objective_coefficients[] = {2.0};

// Constraint bounds
// From the constraints:
// x >= 0.2
// x <= 0.5
cuopt_float_t constraint_upper_bounds[] = {CUOPT_INFINITY, 0.5};
cuopt_float_t constraint_lower_bounds[] = {0.2, -CUOPT_INFINITY};

// Variable bounds - INVALID: lower > upper
// After MOI wrapper processing:
cuopt_float_t var_lower_bounds[] = {1.0}; // ceil(max(0.0, 0.2)) = 1.0
cuopt_float_t var_upper_bounds[] = {0.0}; // floor(min(1.0, 0.5)) = 0.0

// Variable types (binary)
char variable_types[] = {CUOPT_INTEGER}; // Binary variable
if (!test_mip) variable_types[0] = CUOPT_CONTINUOUS;

cuopt_int_t status;
cuopt_float_t time;
cuopt_int_t termination_status;
cuopt_float_t objective_value;

printf("Testing invalid bounds scenario (MOI wrapper issue)...\n");
printf("Problem: Binary variable with bounds 1.0 <= x <= 0.0 (INVALID!)\n");

// Create the problem
status = cuOptCreateRangedProblem(num_constraints,
num_variables,
CUOPT_MAXIMIZE, // maximize
0.0, // objective offset
objective_coefficients,
row_offsets,
column_indices,
values,
constraint_lower_bounds,
constraint_upper_bounds,
var_lower_bounds,
var_upper_bounds,
variable_types,
&problem);

printf("cuOptCreateRangedProblem returned: %d\n", status);

if (status != CUOPT_SUCCESS) {
printf("✗ Unexpected error: %d\n", status);
goto DONE;
}

// If we get here, the problem was created successfully
printf("✓ Problem created successfully\n");

// Create solver settings
status = cuOptCreateSolverSettings(&settings);
if (status != CUOPT_SUCCESS) {
printf("Error creating solver settings: %d\n", status);
goto DONE;
}

// Solve the problem
status = cuOptSolve(problem, settings, &solution);
if (status != CUOPT_SUCCESS) {
printf("Error solving problem: %d\n", status);
goto DONE;
}

// Get solution information
status = cuOptGetSolveTime(solution, &time);
if (status != CUOPT_SUCCESS) {
printf("Error getting solve time: %d\n", status);
goto DONE;
}

status = cuOptGetTerminationStatus(solution, &termination_status);
if (status != CUOPT_SUCCESS) {
printf("Error getting termination status: %d\n", status);
goto DONE;
}
if (termination_status != CUOPT_TERIMINATION_STATUS_INFEASIBLE) {
printf("Error: expected termination status to be %d, but got %d\n",
CUOPT_TERIMINATION_STATUS_INFEASIBLE,
termination_status);
status = CUOPT_VALIDATION_ERROR;
goto DONE;
}
else {
printf("✓ Problem found infeasible as expected\n");
status = CUOPT_SUCCESS;
goto DONE;
}

status = cuOptGetObjectiveValue(solution, &objective_value);
if (status != CUOPT_SUCCESS) {
printf("Error getting objective value: %d\n", status);
goto DONE;
}

// Print results
printf("\nResults:\n");
printf("--------\n");
printf("Termination status: %s (%d)\n", termination_status_to_string(termination_status), termination_status);
printf("Solve time: %f seconds\n", time);
printf("Objective value: %f\n", objective_value);

// Get and print solution variables
cuopt_float_t* solution_values = (cuopt_float_t*)malloc(num_variables * sizeof(cuopt_float_t));
status = cuOptGetPrimalSolution(solution, solution_values);
if (status != CUOPT_SUCCESS) {
printf("Error getting solution values: %d\n", status);
free(solution_values);
goto DONE;
}

printf("\nSolution: \n");
for (cuopt_int_t i = 0; i < num_variables; i++) {
printf("x%d = %f\n", i + 1, solution_values[i]);
}
free(solution_values);

DONE:
cuOptDestroyProblem(&problem);
cuOptDestroySolverSettings(&settings);
cuOptDestroySolution(&solution);

return status;
}
10 changes: 9 additions & 1 deletion cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ TEST(c_api, solve_time_bb_preemption)
CUOPT_SUCCESS);
EXPECT_EQ(termination_status, CUOPT_TERIMINATION_STATUS_OPTIMAL);
EXPECT_GT(solve_time, 0); // solve time should not be equal to 0, even on very simple instances
// solved by B&B before the diversity solver has time to run
// solved by B&B before the diversity solver has time to run
}

TEST(c_api, bad_parameter_name) { EXPECT_EQ(test_bad_parameter_name(), CUOPT_INVALID_ARGUMENT); }
Expand All @@ -112,3 +112,11 @@ TEST(c_api, test_ranged_problem)
EXPECT_EQ(termination_status, CUOPT_TERIMINATION_STATUS_OPTIMAL);
EXPECT_NEAR(objective, 32.0, 1e-3);
}

TEST(c_api, test_invalid_bounds)
{
// Test LP codepath
EXPECT_EQ(test_invalid_bounds(false), CUOPT_SUCCESS);
// Test MIP codepath
EXPECT_EQ(test_invalid_bounds(true), CUOPT_SUCCESS);
}
1 change: 1 addition & 0 deletions cpp/tests/linear_programming/c_api_tests/c_api_tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ cuopt_int_t test_missing_file();
cuopt_int_t test_infeasible_problem();
cuopt_int_t test_bad_parameter_name();
cuopt_int_t test_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr);
cuopt_int_t test_invalid_bounds(cuopt_int_t test_mip);

#ifdef __cplusplus
}
Expand Down
6 changes: 6 additions & 0 deletions cpp/tests/mip/termination_test.cu
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ TEST(termination_status, lower_bound_bb_timeout)
EXPECT_GE(lb, obj_val);
}

TEST(termination_status, crossing_bounds_infeasible)
{
auto [termination_status, obj_val, lb] = test_mps_file("mip/crossing_var_bounds.mps", 0.5, false);
EXPECT_EQ(termination_status, mip_termination_status_t::Infeasible);
}

TEST(termination_status, bb_infeasible_test)
{
// First, check that presolve doesn't reduce the problem to infeasibility
Expand Down
2 changes: 1 addition & 1 deletion cpp/tests/mip/unit_test.cu
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ TEST(ErrorTest, TestError)

// Set constraint bounds
std::vector<double> lower_bounds = {1.0};
std::vector<double> upper_bounds = {0.0};
std::vector<double> upper_bounds = {1.0, 1.0};
Copy link
Contributor

Choose a reason for hiding this comment

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

The sizes are different for lower and upper bounds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a test that is meant to catch is a problem is ill-formed (e.g., contains bounds of different sizes)

problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size());
problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size());

Expand Down
27 changes: 27 additions & 0 deletions datasets/mip/crossing_var_bounds.mps
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
* Optimal solution -28
NAME MIP_SAMPLE
ROWS
N OBJ
L C1
L C2
L C3
COLUMNS
MARK0001 'MARKER' 'INTORG'
X1 OBJ -7
X1 C1 -1
X1 C2 5
X1 C3 -2
X2 OBJ -2
X2 C1 2
X2 C2 1
X2 C3 -2
MARK0001 'MARKER' 'INTEND'
RHS
RHS C1 4
RHS C2 20
RHS C3 -7
BOUNDS
UP BOUND X1 10
LO BOUNDS X1 20
UP BOUND X2 10
ENDATA