NVIDIA / NVIDIA/cuopt

[BUG] Dual simplex logs an incorrect objective after a bound flip

Open
#1,893 1 comment 0 reactions 1 assignee View on GitHub

@chris-maes is already working on this.

Since Sep 13, 2026.

bug
Dominant language
Cuda
Stars
1k
Forks
233
Avg merge
4d 4h
Merged PRs (30d)
95

Description

(Agent-written bug report follows.)

Reproduced on commit 28f247d167ec9b18c7f33bf6492681cb03f30e0d using the public C API.

For the LP

minimize x + 2y
subject to x + y = 2
           0 ≤ x ≤ 1
           y ≥ 0

the optimal solution is x = y = 1, with objective 3. Dual simplex's iteration log reports objective 2 at the final iteration, although the final summary and
returned solution correctly report 3.

Reproduction
#include <cuopt/mathematical_optimization/cuopt_c.h>
#include <cuopt/mathematical_optimization/constants.h>
#include <stdio.h>
#include <stdlib.h>

#define CHECK(call) do {                                      \
  cuopt_int_t rc = (call);                                    \
  if (rc != CUOPT_SUCCESS) {                                  \
    fprintf(stderr, "%s failed: %d\n", #call, rc);             \
    exit(1);                                                 \
  }                                                          \
} while (0)

int main(void)
{
  cuOptOptimizationProblem problem = NULL;
  cuOptSolverSettings settings = NULL;
  cuOptSolution solution = NULL;

  cuopt_int_t offsets[] = {0, 2}, columns[] = {0, 1};
  cuopt_float_t values[] = {1, 1}, objective[] = {1, 2};
  cuopt_float_t row_lower[] = {2}, row_upper[] = {2};
  cuopt_float_t lower[] = {0, 0}, upper[] = {1, CUOPT_INFINITY};
  char types[] = {CUOPT_CONTINUOUS, CUOPT_CONTINUOUS};

  CHECK(cuOptCreateRangedProblem(
    1, 2, CUOPT_MINIMIZE, 0, objective,
    offsets, columns, values, row_lower, row_upper,
    lower, upper, types, &problem));

  CHECK(cuOptCreateSolverSettings(&settings));
  CHECK(cuOptSetIntegerParameter(
    settings, CUOPT_METHOD, CUOPT_METHOD_DUAL_SIMPLEX));
  CHECK(cuOptSetIntegerParameter(settings, CUOPT_PRESOLVE, 0));
  CHECK(cuOptSetIntegerParameter(settings, CUOPT_LOG_TO_CONSOLE, 1));
  CHECK(cuOptSolve(problem, settings, &solution));

  cuopt_int_t status;
  cuopt_float_t obj, x[2];
  CHECK(cuOptGetTerminationStatus(solution, &status));
  CHECK(cuOptGetObjectiveValue(solution, &obj));
  CHECK(cuOptGetPrimalSolution(solution, x));

  printf("PUBLIC_API_RESULT status=%d x=%.17g y=%.17g "
         "objective=%.17g recomputed=%.17g\n",
         status, x[0], x[1], obj, x[0] + 2*x[1]);

  cuOptDestroySolution(&solution);
  cuOptDestroySolverSettings(&settings);
  cuOptDestroyProblem(&problem);
  return 0;
}
Actual output

Relevant excerpt from a run on an NVIDIA RTX PRO 6000 Blackwell Workstation Edition:

cuOpt version: 26.10.0, git hash: 28f247d1, host arch: x86_64, device archs: 120a-real

Dual Simplex Phase 1
Dual feasible solution found.
Dual Simplex Phase 2
 Iter     Objective           Num Inf.  Sum Inf.     Perturb  Time
    0 +0.0000000000000000e+00       1 4.00000000e+00 0.00e+00 0.04
    1 +2.0000000000000000e+00       0 0.00000000e+00 1.00e-07 0.04


Optimal solution found in 1 iterations and 0.04s
Objective +3.00000000e+00


Primal infeasibility (abs): 0.00e+00
Dual infeasibility (abs):   0.00e+00
Perturbation:               1.00e-07
Dual simplex finished in 0.04 seconds
PUBLIC_API_RESULT status=1 x=1 y=1 objective=3 recomputed=3
Expected behavior

The iteration-1 objective should be 3, consistent with the current primal solution and final summary.

Apparent cause

In cpp/src/dual_simplex/phase2.cpp, adjust_for_flips() changes the primal variables without updating the running objective. The subsequent update_objective()
accounts for the pivot displacement but omits the bound-flip displacement.

In this example, flipping x from 0 to 1 contributes the missing +1. This discrepancy is not explained by objective perturbation: the reported perturbation
is only 1e-7.

The final objective is recomputed in prepare_optimality(), which explains why the returned objective and final summary are correct in this reproduction.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.