diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e786f35a59..d20c4d794d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1981,6 +1981,15 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( std::string solver_name = ""; lp_status_t root_status; + auto request_root_concurrent_halt = [this](lp_status_t status) { + if (status == lp_status_t::OPTIMAL || status == lp_status_t::INFEASIBLE || + status == lp_status_t::UNBOUNDED || status == lp_status_t::UNBOUNDED_OR_INFEASIBLE || + status == lp_status_t::ITERATION_LIMIT || status == lp_status_t::TIME_LIMIT || + status == lp_status_t::NUMERICAL_ISSUES || status == lp_status_t::CUTOFF || + status == lp_status_t::WORK_LIMIT) { + set_root_concurrent_halt(1); + } + }; // 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) @@ -1995,6 +2004,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( root_vstatus_, edge_norms_, nullptr); + request_root_concurrent_halt(root_status); } // Wait for the root relaxation solution to be sent by the diversity manager or dual simplex diff --git a/python/libcuopt/libcuopt/tests/test_cli.sh b/python/libcuopt/libcuopt/tests/test_cli.sh index 85f73fa58f..48476adbe2 100644 --- a/python/libcuopt/libcuopt/tests/test_cli.sh +++ b/python/libcuopt/libcuopt/tests/test_cli.sh @@ -31,3 +31,38 @@ cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/linear_programming/good-mps-1.lp.bz2 | gr # Add a for mixed integer programming test with options cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/mip/sample.mps --mip-absolute-gap 0.01 --time-limit 10 | grep -q "Solution objective" || (echo "Expected solution objective not found" && exit 1) + +# Regression for opportunistic concurrent root LP solve on an infeasible MILP. +# Default opportunistic mode should return Infeasible instead of aborting. +tmp_mps="$(mktemp "${TMPDIR:-/tmp}/cuopt-infeasible-milp.XXXXXX.mps")" +trap 'rm -f "${tmp_mps}"' EXIT +python3 - "${tmp_mps}" <<'PY' +import sys + +path = sys.argv[1] +n = 120 +a_vals = [1.0 + ((i * 37) % 90) / 10.0 for i in range(n)] +b_vals = [a + (((i * 17) % 11) - 5.0) / 20.0 for i, a in enumerate(a_vals)] +need_a = 0.8 * sum(a_vals) +cap_b = 0.3 * sum(b_vals) + +with open(path, "w", encoding="utf-8") as f: + f.write("NAME INFEAS_MILP\n") + f.write("ROWS\n") + f.write(" N COST\n") + f.write(" G NEED_A\n") + f.write(" L CAP_B\n") + f.write("COLUMNS\n") + f.write(" M1 'MARKER' 'INTORG'\n") + for i, (a, b) in enumerate(zip(a_vals, b_vals)): + f.write(f" x{i:<8} COST 1 NEED_A {a:.17g}\n") + f.write(f" x{i:<8} CAP_B {b:.17g}\n") + f.write(" M2 'MARKER' 'INTEND'\n") + f.write("RHS\n") + f.write(f" RHS NEED_A {need_a:.17g} CAP_B {cap_b:.17g}\n") + f.write("BOUNDS\n") + for i in range(n): + f.write(f" UP BND x{i:<8} 1\n") + f.write("ENDATA\n") +PY +cuopt_cli "${tmp_mps}" --time-limit 30 --mip-determinism-mode 0 | grep -q "Termination Status: Infeasible" || (echo "Expected infeasible status for opportunistic MILP" && exit 1)