Gurobi12 Reports unbounded to a nonconvex QP which is actually bounded

import JuMP
import Gurobi
function objf(x, y) return (y - 2)^2/4 - x * y end
B = JuMP.Model(() -> Gurobi.Optimizer()); # A bilinear program
JuMP.@variable(B, 0 <= x <= 1);
JuMP.@variable(B, y); # this variable is free
JuMP.@objective(B, Min, objf(x, y));
JuMP.optimize!(B);
JuMP.termination_status(B) # DUAL_INFEASIBLE
# julia> println(B)
# Min 0.25 y² - x*y - y + 1
# Subject to
#  x >= 0
#  x <= 1
# Comment: Theoretically speaking, The program is bounded below because 0.25 is positive
# And this can be reflected by visualizing 
using CairoMakie
f = Figure();
Axis(f[1, 1]);
xs = LinRange(0, 1, 100);
ys = LinRange(-3, 8, 100);
zs = [objf(x, y) for x in xs, y in ys];
contour!(xs, ys, zs; labels=true, levels = -4:0.5:4);
f

Run the above code in julia REPL:

julia> import JuMP

julia> import Gurobi

julia> function objf(x, y) return (y - 2)^2/4 - x * y end
objf (generic function with 1 method)

julia> B = JuMP.Model(() -> Gurobi.Optimizer());
Set parameter Username
Set parameter LicenseID to value 2602363
Academic license - for non-commercial use only - expires 2025-12-20

julia> JuMP.@variable(B, 0 <= x <= 1);

julia> JuMP.@variable(B, y);

julia> JuMP.@objective(B, Min, objf(x, y));

julia> JuMP.optimize!(B);
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (win64 - Windows 11.0 (26100.2))

CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0x64a2e73d
Model has 2 quadratic objective terms
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  Objective range  [1e+00, 1e+00]
  QObjective range [5e-01, 2e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [0e+00, 0e+00]

Continuous model is non-convex -- solving as a MIP

Found heuristic solution: objective 1.0000000
Found heuristic solution: objective 0.8925000
Presolve time: 0.00s
Presolved: 3 rows, 7 columns, 8 nonzeros
Presolved model has 2 SOS constraint(s)
Presolved model has 1 quadratic constraint(s)
Found heuristic solution: objective 0.8025000
Variable types: 5 continuous, 2 integer (2 binary)

Root relaxation: unbounded, 1 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  postponed    0         0.80250          -      -     -    0s
     0     0  postponed    0         0.80250          -      -     -    0s
     0     0  postponed    0         0.80250          -      -     -    0s
     0     2  postponed    0         0.80250          -      -     -    0s

Explored 3 nodes (7 simplex iterations) in 0.02 seconds (0.00 work units)
Thread count was 8 (of 8 available processors)

Solution count 3: 0.8025 0.8925 1

Model is unbounded
Best objective 8.025000000000e-01, best bound 8.025000000000e-01, gap 0.0000%

User-callback calls 123, time in user-callback 0.00 sec
1 Like

This looks like a bug in Gurobi.

julia> using JuMP

julia> import Gurobi

julia> begin
           model = direct_model(Gurobi.Optimizer())
           @variable(model, 0 <= x <= 1)
           @variable(model, y)
           @objective(model, Min, 0.25 * y^2 - x * y)
           optimize!(model)
           termination_status(model)
           Gurobi.GRBwrite(unsafe_backend(model), "model.mps")
       end
Set parameter LicenseID to value 890341
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (mac64[x86] - Darwin 24.1.0 24B83)

CPU model: Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads

Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0x3ae1ad4e
Model has 2 quadratic objective terms
Coefficient statistics:
  Matrix range     [0e+00, 0e+00]
  Objective range  [0e+00, 0e+00]
  QObjective range [5e-01, 2e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [0e+00, 0e+00]

Continuous model is non-convex -- solving as a MIP

Found heuristic solution: objective 0.0000000
Found heuristic solution: objective -0.0075000
Presolve time: 0.00s
Presolved: 3 rows, 7 columns, 8 nonzeros
Presolved model has 2 SOS constraint(s)
Presolved model has 1 quadratic constraint(s)
Found heuristic solution: objective -0.0975000
Variable types: 5 continuous, 2 integer (2 binary)

Root relaxation: unbounded, 1 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  postponed    0        -0.09750          -      -     -    0s
     0     0  postponed    0        -0.09750          -      -     -    0s
     0     2  postponed    0        -0.09750          -      -     -    0s

Explored 3 nodes (4 simplex iterations) in 0.00 seconds (0.00 work units)
Thread count was 8 (of 8 available processors)

Solution count 3: -0.0975 -0.0075 0 

Model is unbounded
Best objective -9.750000000000e-02, best bound -9.750000000000e-02, gap 0.0000%

User-callback calls 119, time in user-callback 0.00 sec
0

shell> cat model.mps
* Signature: 0x9a8e9d963566fb97
NAME 
ROWS
 N  OBJ
COLUMNS
    x         OBJ       0
    y         OBJ       0
RHS
BOUNDS
 UP BND1      x         1
 FR BND1      y       
QUADOBJ
    x         y         -1
    y         y         0.5
ENDATA

Want to open a support ticket with Gurobi? Otherwise I can do it.

Yes please,
please let me know if they have a feedback.

Actually, if you split the problem into 2 subproblems:

  1. y >= 0
  2. y <= 0

Then Gurobi will report OPTIMAL on both of them.
Therefore the primal problem should also be OPTIMAL, where the optimal lower bound is the minimum of the optimal lower bound of the 2 subproblems.

It also suggest that solvers tend to be more stable if we let all of our variables be nonnegative, instead of being free.

I’ve opened a support ticket https://support.gurobi.com/hc/en-us/requests/90093 (which you probably can’t see) and cc’d your @foxmail email (but you will get emails only if you’ve previously registered this email with Gurobi).

It also suggest that solvers tend to be more stable if we let all of our variables be nonnegative, instead of being free.

Unrelated. This is just a straight-up correctness bug in the solver.

1 Like

Just to update this: Gurobi support have reproduced the bug and are investigating a fix.

But note that even if a fix is found quickly, it will take a while before it becomes available as part of their regular release cycle.

2 Likes

I took a closer look at what happened, then I’m shocked.
Aside from the incorrect termination status, It’s full of inconsistency.

# copy my first 8 lines in my first post, (till JuMP.optimize!(B);)
JuMP.solution_summary(B) # then call this to take a look

Nodes    |    Current Node    |     Objective Bounds      |     Work
Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

    0     0  postponed    0         0.80250          -      -     -    0s
    0     0  postponed    0         0.80250          -      -     -    0s
    0     0  postponed    0         0.80250          -      -     -    0s
    0     2  postponed    0         0.80250          -      -     -    0s

Explored 3 nodes (7 simplex iterations) in 0.01 seconds (0.00 work units)
Thread count was 8 (of 8 available processors)

Solution count 3: 0.8025 0.8925 1

Model is unbounded
Best objective 8.025000000000e-01, best bound 8.025000000000e-01, gap 0.0000% # 🥎

User-callback calls 122, time in user-callback 0.00 sec

julia> JuMP.solution_summary(B)
* Status
 Result count       : 3 # 🍅
 Termination status : DUAL_INFEASIBLE
 Message from the solver:
 "Model was proven to be unbounded. Important note: an unbounded status indicates the presence of an unbounded ray that allows the objective to improve without limit. It says nothing about whether the model has a feasible solution. If you require information on feasibility, you should set the objective to zero and reoptimize."

* Candidate solution (result #1)
 Primal status      : NO_SOLUTION # It seems to be 3 solutions, see 🍅
 Dual status        : NO_SOLUTION
 Objective value    : 8.02500e-01 # this value matches Gurobi's, see 🥎's line
 Objective bound    : 1.00000e+100 # this value does NOT EVEN match Gurobi's `best bound`, see 🥎's line, which doesn't make any sense
 Dual objective value : 1.00000e+100

Oh, I know what happened!

I have a fix in Fix PrimalStatus when multiple solutions are available in DUAL_INFEASIBLE by odow · Pull Request #623 · jump-dev/Gurobi.jl · GitHub to report the primal status correctly.

Other than that: yes, Gurobi have acknowledged there is a bug, so it’s not surprising that the log is inconsistent.

Their actions are quite slow. (It appears I’m not the first to find their bug, it appears other people not in julia community had reported it). Yet they didn’t release a new verison :enraged_face:.

(For readers, the discussion jumps to About JuMP.is_solved_and_feasible - #17 by WalterMadelim)

Gurobi reproduced the bug within hours of the report, which seems quite fast :smile: It has since been 6 days, and Gurobi’s cycle time is measured in months. (They don’t have a continuous release process like JuMP or Gurobi.jl.)

As a general comment: it’s great to see you so actively engaging in the forum, but let’s try to keep these posts positive and focused on the software part of JuMP and Julia that we control. Always assume that people are engaging and working with the best of intentions. Constructive criticism and questions are helpful “why is it like this?” “Could it be this instead?”. Strongly worded statements are less helpful.

1 Like