Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 20 additions & 10 deletions cpp/src/branch_and_bound/branch_and_bound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <limits>
#include <string>
#include <vector>
Expand Down Expand Up @@ -1981,20 +1982,26 @@ lp_status_t branch_and_bound_t<i_t, f_t>::solve_root_relaxation(
std::string solver_name = "";

lp_status_t root_status;
std::exception_ptr root_exception;

// Launch a task for solving the root LP relaxation via dual simplex.
#pragma omp task default(shared) depend(out : root_status) priority(CUOPT_CRITICAL_TASK_PRIORITY)
{
root_status = solve_linear_program_with_advanced_basis(original_lp_,
exploration_stats_.start_time,
lp_settings,
root_relax_soln_,
basis_update,
basic_list,
nonbasic_list,
root_vstatus_,
edge_norms_,
nullptr);
try {
root_status = solve_linear_program_with_advanced_basis(original_lp_,
exploration_stats_.start_time,
lp_settings,
root_relax_soln_,
basis_update,
basic_list,
nonbasic_list,
root_vstatus_,
edge_norms_,
nullptr);
} catch (...) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the underlying exception we are catching here? It might be best to address the root cause of that exception.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed that the root cause is the ideal fix if the repro exposes a specific solver exception. From the current public repro, the observable failure is process abort from an exception escaping OpenMP task execution in the opportunistic concurrent root path.

I added the exact Python repro as a subprocess-based pytest so NVIDIA CI can now validate the end-to-end path and expose whether there is still a deeper solver exception after task synchronization is fixed. If that test fails with a concrete exception/status, I will follow up by addressing that root cause rather than leaving this as only exception containment.

root_exception = std::current_exception();
set_root_concurrent_halt(1);
}
}

// Wait for the root relaxation solution to be sent by the diversity manager or dual simplex
Expand Down Expand Up @@ -2040,6 +2047,7 @@ lp_status_t branch_and_bound_t<i_t, f_t>::solve_root_relaxation(
// Stop dual simplex and then wait it to finish
set_root_concurrent_halt(1);
#pragma omp taskwait depend(in : root_status)
if (root_exception) { std::rethrow_exception(root_exception); }

set_root_concurrent_halt(0); // Clear the concurrent halt flag
// Override the root relaxation solution with the crossover solution
Expand Down Expand Up @@ -2093,6 +2101,7 @@ lp_status_t branch_and_bound_t<i_t, f_t>::solve_root_relaxation(
} else {
// Wait for the dual simplex to finish (after telling PDLP/Barrier to stop)
#pragma omp taskwait depend(in : root_status)
if (root_exception) { std::rethrow_exception(root_exception); }
user_objective = root_relax_soln_.user_objective;
iter = root_relax_soln_.iterations;
root_relax_solved_by = DualSimplex;
Expand All @@ -2101,6 +2110,7 @@ lp_status_t branch_and_bound_t<i_t, f_t>::solve_root_relaxation(
} else {
// Wait for the dual simplex to finish (crossover do not produced a solution)
#pragma omp taskwait depend(in : root_status)
if (root_exception) { std::rethrow_exception(root_exception); }
user_objective = root_relax_soln_.user_objective;
iter = root_relax_soln_.iterations;
root_relax_solved_by = DualSimplex;
Expand Down
12 changes: 11 additions & 1 deletion cpp/src/mip_heuristics/solver.cu
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <raft/core/cusparse_macros.hpp>

#include <cmath>
#include <exception>
#include <future>
#include <memory>
#include <thread>
Expand Down Expand Up @@ -490,12 +491,19 @@ solution_t<i_t, f_t> mip_solver_t<i_t, f_t>::run_solver()
}
}

std::exception_ptr branch_and_bound_exception;

#pragma omp taskgroup
{
if (!context.settings.heuristics_only) {
#pragma omp task default(shared) priority(CUOPT_CRITICAL_TASK_PRIORITY)
{
branch_and_bound_status = branch_and_bound->solve(branch_and_bound_solution);
try {
branch_and_bound_status = branch_and_bound->solve(branch_and_bound_solution);
} catch (...) {
branch_and_bound_exception = std::current_exception();
solution_helper.preempt_heuristic_solver();
}
}
}

Expand All @@ -504,6 +512,8 @@ solution_t<i_t, f_t> mip_solver_t<i_t, f_t>::run_solver()
sol = dm.run_solver();
} // implicit barrier for all tasks created in B&B and heuristics

if (branch_and_bound_exception) { std::rethrow_exception(branch_and_bound_exception); }

if (!context.settings.heuristics_only) {
if (branch_and_bound_solution.lower_bound > -std::numeric_limits<f_t>::infinity()) {
context.stats.set_solution_bound(
Expand Down
71 changes: 71 additions & 0 deletions cpp/tests/mip/termination_test.cu
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,62 @@ namespace cuopt::linear_programming::test {
constexpr double default_time_limit = 10;
constexpr bool default_heuristics_only = true;

namespace {

optimization_problem_t<int, double> make_concurrent_root_infeasible_problem(
raft::handle_t const* handle)
{
constexpr int n = 120;

optimization_problem_t<int, double> problem(handle);
std::vector<double> coefficients;
std::vector<int> indices;
coefficients.reserve(2 * n);
indices.reserve(2 * n);
std::vector<int> offsets = {0, n, 2 * n};
std::vector<double> objective(n, 1.0);
std::vector<double> var_lower(n, 0.0);
std::vector<double> var_upper(n, 1.0);
std::vector<var_t> var_types(n, var_t::INTEGER);

double sum_a = 0.0;
double sum_b = 0.0;
for (int i = 0; i < n; ++i) {
const double a = 1.0 + static_cast<double>((i * 37) % 90) / 10.0;
const double b = a + (static_cast<double>((i * 17) % 11) - 5.0) / 20.0;
coefficients.push_back(a);
indices.push_back(i);
sum_a += a;
sum_b += b;
}
for (int i = 0; i < n; ++i) {
const double a = 1.0 + static_cast<double>((i * 37) % 90) / 10.0;
const double b = a + (static_cast<double>((i * 17) % 11) - 5.0) / 20.0;
coefficients.push_back(b);
indices.push_back(i);
}

const std::vector<double> row_lower = {0.80 * sum_a, -std::numeric_limits<double>::infinity()};
const std::vector<double> row_upper = {std::numeric_limits<double>::infinity(), 0.30 * sum_b};

problem.set_csr_constraint_matrix(coefficients.data(),
coefficients.size(),
indices.data(),
indices.size(),
offsets.data(),
offsets.size());
problem.set_constraint_lower_bounds(row_lower.data(), row_lower.size());
problem.set_constraint_upper_bounds(row_upper.data(), row_upper.size());
problem.set_objective_coefficients(objective.data(), objective.size());
problem.set_variable_lower_bounds(var_lower.data(), var_lower.size());
problem.set_variable_upper_bounds(var_upper.data(), var_upper.size());
problem.set_variable_types(var_types.data(), var_types.size());

return problem;
}

} // namespace

TEST(termination_status, trivial_presolve_optimality_test)
{
auto [termination_status, obj_val, lb] = test_mps_file(
Expand Down Expand Up @@ -132,4 +188,19 @@ TEST(termination_status, bb_infeasible_test)
}
}

TEST(termination_status, concurrent_root_infeasible_returns_status)
{
const raft::handle_t handle_{};
auto problem = make_concurrent_root_infeasible_problem(&handle_);
handle_.sync_stream();

mip_solver_settings_t<int, double> settings;
settings.time_limit = 15.0;
settings.determinism_mode = CUOPT_MODE_OPPORTUNISTIC;
settings.num_cpu_threads = 8;

auto solution = solve_mip(&handle_, problem, settings);
EXPECT_EQ(solution.get_termination_status(), mip_termination_status_t::Infeasible);
}

} // namespace cuopt::linear_programming::test
68 changes: 68 additions & 0 deletions python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import math
import os
import subprocess
import sys
import textwrap
from enum import IntEnum

from cuopt.linear_programming import Read
Expand Down Expand Up @@ -101,6 +104,71 @@ def test_parser_and_solver():
assert solution.get_termination_reason() == "Optimal"


def test_mip_opportunistic_infeasible_repro_returns_status():
"""Regression for #1396: opportunistic infeasible MILP must not abort.

The worker script mirrors the issue repro and subprocess.run isolates each
mode so process-level crashes become test failures. Both deterministic and
opportunistic runs must exit successfully and print STATUS=Infeasible.
"""
worker = textwrap.dedent(
"""
import sys
import numpy as np
from cuopt.linear_programming.problem import Problem, INTEGER, MINIMIZE
from cuopt.linear_programming import SolverSettings
from cuopt.linear_programming.solver.solver_parameters import (
CUOPT_MIP_DETERMINISM_MODE,
CUOPT_TIME_LIMIT,
)

mode = sys.argv[1]
rng = np.random.default_rng(0)
n = 120
a = rng.uniform(1.0, 10.0, n)
b = a + rng.uniform(-0.5, 0.5, n)
need_a = 0.80 * a.sum()
cap_b = 0.30 * b.sum()

p = Problem("infeasible_milp")
x = [
p.addVariable(lb=0.0, ub=1.0, vtype=INTEGER, name=f"x{i}")
for i in range(n)
]
p.setObjective(sum(x), sense=MINIMIZE)
p.addConstraint(
sum(float(a[i]) * x[i] for i in range(n)) >= float(need_a),
name="need_a",
)
p.addConstraint(
sum(float(b[i]) * x[i] for i in range(n)) <= float(cap_b),
name="cap_b",
)

s = SolverSettings()
s.set_parameter(CUOPT_TIME_LIMIT, 15.0)
if mode == "deterministic":
s.set_parameter(CUOPT_MIP_DETERMINISM_MODE, 1)
p.solve(s)
print("STATUS=" + str(getattr(p.Status, "name", p.Status)))
"""
)

for mode in ["deterministic", "opportunistic"]:
result = subprocess.run(
[sys.executable, "-c", worker, mode],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, (
f"{mode} exited {result.returncode}\n"
f"stdout:\n{result.stdout}\n"
f"stderr:\n{result.stderr}"
)
assert "STATUS=Infeasible" in result.stdout


def test_set_get_fields():
data_model_obj = data_model.DataModel()

Expand Down