Skip to content

Handle cuOpt UnboundedOrInfeasible termination status (11)#3916

Open
rgsl888prabhu wants to merge 5 commits intoPyomo:mainfrom
rgsl888prabhu:cuopt-unboundedorinfeasible-status
Open

Handle cuOpt UnboundedOrInfeasible termination status (11)#3916
rgsl888prabhu wants to merge 5 commits intoPyomo:mainfrom
rgsl888prabhu:cuopt-unboundedorinfeasible-status

Conversation

@rgsl888prabhu
Copy link
Copy Markdown

Summary

cuOpt added a new termination status (value 11, `UnboundedOrInfeasible`) that its presolver returns when it cannot disambiguate infeasibility from unboundedness. The `cuopt_direct` plugin's status cascade does not recognize it, so it falls through to `TerminationCondition.error` and fails the `LP_unbounded` test variants (`test_cuopt_python`, `test_cuopt_python_nonsymbolic_labels`, `test_cuopt_python_symbolic_labels`).

Adds an `elif status == 11` branch mapping to `TerminationCondition.infeasibleOrUnbounded` (with `SolverStatus.warning` / `SolutionStatus.unsure`). Also extends the status-code comment block to include status 10 (WorkLimit) and 11 (UnboundedOrInfeasible).

Tracked upstream in NVIDIA/cuopt#1114.

cuOpt added a new termination status (value 11, UnboundedOrInfeasible)
that the PSLP presolver returns when it cannot disambiguate infeasibility
from unboundedness. The cuopt_direct plugin's status cascade did not
recognize it, falling through to TerminationCondition.error and failing
any LP_unbounded test.

Adds an elif branch mapping status 11 to TerminationCondition.
infeasibleOrUnbounded (with SolverStatus.warning / SolutionStatus.unsure).
Also extends the status-code comment block to include status 10 (WorkLimit)
and 11 (UnboundedOrInfeasible) for documentation.

Tracked in NVIDIA/cuopt#1114.

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
@jsiirola
Copy link
Copy Markdown
Member

This looks reasonable. Can you add a a test that exercises this result from cuOpt?

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 2026

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.93%. Comparing base (34a3877) to head (326e154).

Files with missing lines Patch % Lines
pyomo/solvers/plugins/solvers/cuopt_direct.py 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3916      +/-   ##
==========================================
- Coverage   89.94%   89.93%   -0.01%     
==========================================
  Files         902      902              
  Lines      106457   106461       +4     
==========================================
- Hits        95748    95745       -3     
- Misses      10709    10716       +7     
Flag Coverage Δ
builders 29.18% <0.00%> (+<0.01%) ⬆️
default 86.24% <0.00%> (?)
expensive 35.64% <0.00%> (?)
linux 87.39% <0.00%> (-2.04%) ⬇️
linux_other 87.39% <0.00%> (-0.01%) ⬇️
oldsolvers 28.11% <0.00%> (+<0.01%) ⬆️
osx 82.72% <0.00%> (-0.01%) ⬇️
win 85.81% <0.00%> (-0.02%) ⬇️
win_other 85.81% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

rgsl888prabhu and others added 2 commits April 17, 2026 13:19
An unbounded LP with no variable bounds triggers cuOpt's presolver
to return UnboundedOrInfeasible (status 11); the test asserts the
plugin maps it to TerminationCondition.infeasibleOrUnbounded,
SolverStatus.warning, and SolutionStatus.unsure.
blnicho
blnicho previously approved these changes Apr 17, 2026
@jsiirola
Copy link
Copy Markdown
Member

Note: the test appears to require a specific version of cuOpt:

18:21:10 ________________ CUOPTTests.test_unbounded_or_infeasible_status ________________
18:21:10 
18:21:10 self = <pyomo.solvers.tests.checks.test_cuopt_direct.CUOPTTests testMethod=test_unbounded_or_infeasible_status>
18:21:10 
18:21:10     @unittest.skipIf(not cuopt_available, "The CuOpt solver is not available")
18:21:10     def test_unbounded_or_infeasible_status(self):
18:21:10         # An LP with no variable bounds and an unbounded objective triggers
18:21:10         # cuOpt's presolver to return UnboundedOrInfeasible (status 11), which
18:21:10         # the plugin maps to TerminationCondition.infeasibleOrUnbounded.
18:21:10         m = ConcreteModel()
18:21:10         m.x = Var()
18:21:10         m.y = Var()
18:21:10         m.obj = Objective(expr=m.x + m.y, sense=minimize)
18:21:10     
18:21:10         opt = SolverFactory('cuopt')
18:21:10         res = opt.solve(m, load_solutions=False)
18:21:10     
18:21:10 >       self.assertEqual(res.solver.termination_condition, "infeasibleOrUnbounded")
18:21:10 E       AssertionError: <TerminationCondition.unbounded: 'unbounded'> != 'infeasibleOrUnbounded'
18:21:10 
18:21:10 pyomo/pyomo/solvers/tests/checks/test_cuopt_direct.py:141: AssertionError
18:21:10 ----------------------------- Captured stdout call -----------------------------
18:21:10 Setting parameter log_file to /tmp/tmpalrh5rvw.log
18:21:10 cuOpt version: 25.10.0, git hash: 99e549c, host arch: x86_64, device archs: 70-real,75-real,80-real,86-real,90a-real,100f-real,120a-real,120
18:21:10 CPU: Intel(R) Xeon(R) Silver 4410Y, threads (physical/logical): 12/24, RAM: 4.94 GiB
18:21:10 CUDA 12.9, device: NVIDIA T400 4GB (ID 0), VRAM: 3.63 GiB
18:21:10 CUDA device UUID: 
18:21:10 
18:21:10 Solving a problem with 1 constraints, 2 variables (0 integers), and 1 nonzeros
18:21:10 Problem scaling:
18:21:10 Objective coefficents range:          [1e+00, 1e+00]
18:21:10 Constraint matrix coefficients range: [0e+00, 0e+00]
18:21:10 Constraint rhs / bounds range:        [0e+00, 0e+00]
18:21:10 Variable bounds range:                [0e+00, 0e+00]
18:21:10 
18:21:10 Third-party presolve is disabled, skipping
18:21:10 Objective offset 0.000000 scaling_factor 1.000000
18:21:10 Running concurrent
18:21:10 
18:21:10 Dual simplex finished in 0.00 seconds, total time 0.00
18:21:10 Barrier finished in 0.00 seconds
18:21:10    Iter    Primal Obj.      Dual Obj.    Gap        Primal Res.  Dual Res.   Time
18:21:10       0 +0.00000000e+00 +0.00000000e+00  0.00e+00   0.00e+00     1.41e+00   0.017s
18:21:10 PDLP finished
18:21:10 Dual Simplex Solve status Dual Infeasible
18:21:10 Concurrent time:  0.019s, total time 0.019s
18:21:10 Solved with dual simplex
18:21:10 Status: Dual Infeasible   Objective: -inf  Iterations: 0  Time: 0.019s

@rgsl888prabhu
Copy link
Copy Markdown
Author

Note: the test appears to require a specific version of cuOpt:

18:21:10 ________________ CUOPTTests.test_unbounded_or_infeasible_status ________________
18:21:10 
18:21:10 self = <pyomo.solvers.tests.checks.test_cuopt_direct.CUOPTTests testMethod=test_unbounded_or_infeasible_status>
18:21:10 
18:21:10     @unittest.skipIf(not cuopt_available, "The CuOpt solver is not available")
18:21:10     def test_unbounded_or_infeasible_status(self):
18:21:10         # An LP with no variable bounds and an unbounded objective triggers
18:21:10         # cuOpt's presolver to return UnboundedOrInfeasible (status 11), which
18:21:10         # the plugin maps to TerminationCondition.infeasibleOrUnbounded.
18:21:10         m = ConcreteModel()
18:21:10         m.x = Var()
18:21:10         m.y = Var()
18:21:10         m.obj = Objective(expr=m.x + m.y, sense=minimize)
18:21:10     
18:21:10         opt = SolverFactory('cuopt')
18:21:10         res = opt.solve(m, load_solutions=False)
18:21:10     
18:21:10 >       self.assertEqual(res.solver.termination_condition, "infeasibleOrUnbounded")
18:21:10 E       AssertionError: <TerminationCondition.unbounded: 'unbounded'> != 'infeasibleOrUnbounded'
18:21:10 
18:21:10 pyomo/pyomo/solvers/tests/checks/test_cuopt_direct.py:141: AssertionError
18:21:10 ----------------------------- Captured stdout call -----------------------------
18:21:10 Setting parameter log_file to /tmp/tmpalrh5rvw.log
18:21:10 cuOpt version: 25.10.0, git hash: 99e549c, host arch: x86_64, device archs: 70-real,75-real,80-real,86-real,90a-real,100f-real,120a-real,120
18:21:10 CPU: Intel(R) Xeon(R) Silver 4410Y, threads (physical/logical): 12/24, RAM: 4.94 GiB
18:21:10 CUDA 12.9, device: NVIDIA T400 4GB (ID 0), VRAM: 3.63 GiB
18:21:10 CUDA device UUID: 
18:21:10 
18:21:10 Solving a problem with 1 constraints, 2 variables (0 integers), and 1 nonzeros
18:21:10 Problem scaling:
18:21:10 Objective coefficents range:          [1e+00, 1e+00]
18:21:10 Constraint matrix coefficients range: [0e+00, 0e+00]
18:21:10 Constraint rhs / bounds range:        [0e+00, 0e+00]
18:21:10 Variable bounds range:                [0e+00, 0e+00]
18:21:10 
18:21:10 Third-party presolve is disabled, skipping
18:21:10 Objective offset 0.000000 scaling_factor 1.000000
18:21:10 Running concurrent
18:21:10 
18:21:10 Dual simplex finished in 0.00 seconds, total time 0.00
18:21:10 Barrier finished in 0.00 seconds
18:21:10    Iter    Primal Obj.      Dual Obj.    Gap        Primal Res.  Dual Res.   Time
18:21:10       0 +0.00000000e+00 +0.00000000e+00  0.00e+00   0.00e+00     1.41e+00   0.017s
18:21:10 PDLP finished
18:21:10 Dual Simplex Solve status Dual Infeasible
18:21:10 Concurrent time:  0.019s, total time 0.019s
18:21:10 Solved with dual simplex
18:21:10 Status: Dual Infeasible   Objective: -inf  Iterations: 0  Time: 0.019s

Status 11 was introduced in cuOpt v26.04.00 (PR #941, tagged 2026-04-09). The CI runs 25.10.0, so PSLP isn't available and status 11 is never produced

@jsiirola
Copy link
Copy Markdown
Member

There are two things that are holding up this PR:

  • the test needs to be updated with a guard to skip if cuOpt is too old (@rgsl888prabhu)
  • the Jenkins infrastructure needs to be updated to test both new and old cuOpt (@jsiirola)

@blnicho blnicho dismissed their stale review April 29, 2026 05:35

Failing tests need to be addressed

@blnicho blnicho marked this pull request as draft April 29, 2026 05:36
@jsiirola
Copy link
Copy Markdown
Member

We are adopting a new review process where we convert PRs that are "waiting on the author" back to "draft" (to signal the PR state to both the author and the dev team). Please use the "Ready for review" button to signal the developers when it is time to get the PR back into the review queue.

cuOpt status 11 (UnboundedOrInfeasible) is produced by the presolver
introduced in cuOpt 26.04. On older cuOpt the LP in the test reaches
the simplex/PDLP path and is reported as TerminationCondition.unbounded
instead, failing the assertion. Guard the test with a runtime version
check via CUOPTDirect._version.

Addresses review feedback on PR Pyomo#3916.
@rgsl888prabhu rgsl888prabhu marked this pull request as ready for review April 29, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants