Describe the bug
Solving an infeasible MILP with default settings aborts the process via std::terminate ("terminate called without an active exception", SIGABRT) instead of returning an Infeasible status. The abort is specific to the concurrent LP root-relaxation path, which is active in opportunistic determinism mode — the default. Setting mip_determinism_mode = 1 (deterministic) returns Infeasible cleanly on the identical model. In a hosted kernel (Jupyter/Colab) the abort kills and restarts the kernel; being a C++ abort it is uncatchable from Python.
Distinct from but likely related to #1021 (also concurrent-mode root-relaxation infeasibility — that one a false infeasible + hang; this one a genuine infeasible that crashes instead of reporting it).
Steps/Code to reproduce
Minimal self-contained A/B (cuOpt + numpy only): one infeasible 120-variable MILP solved twice, toggling only mip_determinism_mode, each variant in its own subprocess so the abort is observable without killing the parent. Output on cuOpt 26.4.0 / NVIDIA L4 / CUDA 12.9:
deterministic -> exit 0, STATUS Infeasible
opportunistic -> exit -6 SIGABRT, terminate called without an active exception
repro script
import subprocess, sys, textwrap
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) # correlated second weight vector
need_a = 0.80 * a.sum() # capture >=80% of a-value ...
cap_b = 0.30 * b.sum() # ... using <=30% of b-value -> infeasible (a~b)
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"]:
r = subprocess.run([sys.executable, "-c", WORKER, mode], capture_output=True, text=True)
out = r.stdout.strip() or (r.stderr.strip().splitlines() or [""])[-1]
print(f"{mode:>13} -> exit {r.returncode:<4} | {out}")
Expected behavior
Return Infeasible as a status (as the sequential / deterministic path already does), not abort the process.
Environment details
- Environment location: Cloud (Google Colab)
- OS: Linux
- Hardware: NVIDIA L4, CUDA 12.9
- Method of cuOpt install: pip (
cuopt-cu12)
- cuOpt Version: 26.4.0 (git hash
d9b7c96a)
Additional context
Workaround (confirmed by the A/B above): mip_determinism_mode = 1.
Hypothesis from a read of main (for triage — not verified at the line level): opportunistic mode enables the concurrent root solve (mip_heuristics/solver.cu — determinism_mode == OPPORTUNISTIC -> set_concurrent_lp_root_solve(true)). In branch_and_bound.cpp (solve_root_relaxation) the dual-simplex worker is spawned as an OpenMP task whose explicit join (set_root_concurrent_halt + #pragma omp taskwait) sits on the crossover-OPTIMAL branch; an infeasible model produces no crossover solution, so that branch appears to be skipped. "terminate ... without an active exception" is the signature of a worker torn down while still joinable rather than a propagating C++ exception.
Describe the bug
Solving an infeasible MILP with default settings aborts the process via
std::terminate("terminate called without an active exception",SIGABRT) instead of returning anInfeasiblestatus. The abort is specific to the concurrent LP root-relaxation path, which is active in opportunistic determinism mode — the default. Settingmip_determinism_mode = 1(deterministic) returnsInfeasiblecleanly on the identical model. In a hosted kernel (Jupyter/Colab) the abort kills and restarts the kernel; being a C++ abort it is uncatchable from Python.Distinct from but likely related to #1021 (also concurrent-mode root-relaxation infeasibility — that one a false infeasible + hang; this one a genuine infeasible that crashes instead of reporting it).
Steps/Code to reproduce
Minimal self-contained A/B (cuOpt + numpy only): one infeasible 120-variable MILP solved twice, toggling only
mip_determinism_mode, each variant in its own subprocess so the abort is observable without killing the parent. Output on cuOpt 26.4.0 / NVIDIA L4 / CUDA 12.9:repro script
Expected behavior
Return
Infeasibleas a status (as the sequential / deterministic path already does), not abort the process.Environment details
cuopt-cu12)d9b7c96a)Additional context
Workaround (confirmed by the A/B above):
mip_determinism_mode = 1.Hypothesis from a read of
main(for triage — not verified at the line level): opportunistic mode enables the concurrent root solve (mip_heuristics/solver.cu—determinism_mode == OPPORTUNISTIC->set_concurrent_lp_root_solve(true)). Inbranch_and_bound.cpp(solve_root_relaxation) the dual-simplex worker is spawned as an OpenMP task whose explicit join (set_root_concurrent_halt+#pragma omp taskwait) sits on the crossover-OPTIMALbranch; an infeasible model produces no crossover solution, so that branch appears to be skipped. "terminate ... without an active exception" is the signature of a worker torn down while still joinable rather than a propagating C++ exception.