Skip to content

Fix a bug in dual variables and reduced costs when we add implied bounds on free variables#1133

Closed
chris-maes wants to merge 38 commits into
NVIDIA:mainfrom
chris-maes:dual_post_solve_bounded_free_vars
Closed

Fix a bug in dual variables and reduced costs when we add implied bounds on free variables#1133
chris-maes wants to merge 38 commits into
NVIDIA:mainfrom
chris-maes:dual_post_solve_bounded_free_vars

Conversation

@chris-maes
Copy link
Copy Markdown
Contributor

When -inf <= x_j <= inf and we add implied bounds on x_j, we will compute a dual solution (u, w) that satisfies

A^T u + w = c + Qx

but w_j could be nonzero. When x_j is free, we need to enforce that the corresponding reduced cost is zero. We do this by transforming (u, w) into (y, z) such that
A^T y + z = c + Qx
with z_j = 0 for all j such that -inf < x_j < inf.

We also add a unit test on a small QP to confirm the fix.

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented Apr 22, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@chris-maes chris-maes self-assigned this Apr 22, 2026
@chris-maes chris-maes added bug Something isn't working non-breaking Introduces a non-breaking change labels Apr 22, 2026
@chris-maes chris-maes added this to the 26.06 milestone Apr 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c6b24384-3e41-4399-be5e-627f57aa8015

📥 Commits

Reviewing files that changed from the base of the PR and between c29cc5e and ffc200d.

📒 Files selected for processing (258)
  • .claude-plugin/marketplace.json
  • .github/workflows/build.yaml
  • .github/workflows/build_images.yaml
  • .github/workflows/build_test_publish_images.yaml
  • .github/workflows/cloud_ci.yaml
  • .github/workflows/inactivity_reminder.yaml
  • .github/workflows/issue_automation.yaml
  • .github/workflows/nightly-summary.yaml
  • .github/workflows/nightly.yaml
  • .github/workflows/pr.yaml
  • .github/workflows/self_hosted_service_test.yaml
  • .github/workflows/test.yaml
  • .github/workflows/test_images.yaml
  • .github/workflows/trigger-breaking-change-alert.yaml
  • .github/zizmor.yml
  • .pre-commit-config.yaml
  • AGENTS.md
  • CONTRIBUTING.md
  • benchmarks/linear_programming/cuopt/benchmark_helper.hpp
  • benchmarks/linear_programming/cuopt/initial_problem_check.hpp
  • benchmarks/linear_programming/cuopt/run_mip.cpp
  • benchmarks/linear_programming/cuopt/run_pdlp.cu
  • build.sh
  • ci/build_python.sh
  • ci/build_wheel_cuopt.sh
  • ci/build_wheel_cuopt_mps_parser.sh
  • ci/build_wheel_libcuopt.sh
  • ci/docker/Dockerfile
  • ci/release/update-version.sh
  • ci/run_ctests.sh
  • ci/run_cuopt_pytests.sh
  • ci/run_cuopt_server_pytests.sh
  • ci/test_cpp.sh
  • ci/test_python.sh
  • ci/test_self_hosted_service.sh
  • ci/test_wheel_cuopt.sh
  • ci/test_wheel_cuopt_server.sh
  • ci/thirdparty-testing/run_cvxpy_tests.sh
  • ci/thirdparty-testing/run_pulp_tests.sh
  • ci/thirdparty-testing/run_pyomo_tests.sh
  • ci/utils/crash_helpers.sh
  • ci/utils/generate_slack_payloads.py
  • ci/utils/install_boost_tbb.sh
  • ci/validate_wheel.sh
  • cmake/RAPIDS.cmake
  • conda/environments/all_cuda-129_arch-aarch64.yaml
  • conda/environments/all_cuda-129_arch-x86_64.yaml
  • conda/environments/all_cuda-132_arch-aarch64.yaml
  • conda/environments/all_cuda-132_arch-x86_64.yaml
  • conda/recipes/cuopt/conda_build_config.yaml
  • conda/recipes/cuopt/recipe.yaml
  • conda/recipes/libcuopt/conda_build_config.yaml
  • conda/recipes/libcuopt/recipe.yaml
  • conda/recipes/mps-parser/conda_build_config.yaml
  • conda/recipes/mps-parser/recipe.yaml
  • cpp/CMakeLists.txt
  • cpp/README.md
  • cpp/cuopt_cli.cpp
  • cpp/include/cuopt/linear_programming/io/data_model_view.hpp
  • cpp/include/cuopt/linear_programming/io/mps_data_model.hpp
  • cpp/include/cuopt/linear_programming/io/mps_writer.hpp
  • cpp/include/cuopt/linear_programming/io/parser.hpp
  • cpp/include/cuopt/linear_programming/io/utilities/cython_mps_parser.hpp
  • cpp/include/cuopt/linear_programming/io/writer.hpp
  • cpp/include/cuopt/linear_programming/optimization_problem.hpp
  • cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp
  • cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp
  • cpp/include/cuopt/linear_programming/solve.hpp
  • cpp/include/cuopt/linear_programming/utilities/cython_solve.hpp
  • cpp/libmps_parser/CMakeLists.txt
  • cpp/libmps_parser/cmake/thirdparty/get_gtest.cmake
  • cpp/libmps_parser/src/utilities/cython_mps_parser.cpp
  • cpp/libmps_parser/tests/CMakeLists.txt
  • cpp/libmps_parser/tests/utilities/common_utils.hpp
  • cpp/src/CMakeLists.txt
  • cpp/src/branch_and_bound/CMakeLists.txt
  • cpp/src/branch_and_bound/branch_and_bound.cpp
  • cpp/src/branch_and_bound/branch_and_bound.hpp
  • cpp/src/branch_and_bound/constants.hpp
  • cpp/src/branch_and_bound/deterministic_workers.hpp
  • cpp/src/branch_and_bound/diving_heuristics.cpp
  • cpp/src/branch_and_bound/mip_node.cpp
  • cpp/src/branch_and_bound/mip_node.hpp
  • cpp/src/branch_and_bound/pseudo_costs.cpp
  • cpp/src/branch_and_bound/pseudo_costs.hpp
  • cpp/src/branch_and_bound/worker.hpp
  • cpp/src/branch_and_bound/worker_pool.hpp
  • cpp/src/cuts/cuts.cpp
  • cpp/src/cuts/cuts.hpp
  • cpp/src/dual_simplex/presolve.cpp
  • cpp/src/dual_simplex/presolve.hpp
  • cpp/src/grpc/cuopt_remote.proto
  • cpp/src/io/CMakeLists.txt
  • cpp/src/io/data_model_view.cpp
  • cpp/src/io/mps_data_model.cpp
  • cpp/src/io/mps_parser.cpp
  • cpp/src/io/mps_parser_internal.hpp
  • cpp/src/io/mps_writer.cpp
  • cpp/src/io/parser.cpp
  • cpp/src/io/utilities/cython_mps_parser.cpp
  • cpp/src/io/utilities/error.hpp
  • cpp/src/io/writer.cpp
  • cpp/src/mip_heuristics/diversity/diversity_manager.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cu
  • cpp/src/mip_heuristics/diversity/lns/rins.cuh
  • cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh
  • cpp/src/mip_heuristics/feasibility_jump/cpu_fj_thread.cuh
  • cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu
  • cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh
  • cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu
  • cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cuh
  • cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh
  • cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu
  • cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh
  • cpp/src/mip_heuristics/local_search/local_search.cu
  • cpp/src/mip_heuristics/local_search/local_search.cuh
  • cpp/src/mip_heuristics/mip_constants.hpp
  • cpp/src/mip_heuristics/presolve/bounds_presolve.cuh
  • cpp/src/mip_heuristics/presolve/conditional_bound_strengthening.cu
  • cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu
  • cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh
  • cpp/src/mip_heuristics/presolve/probing_cache.cu
  • cpp/src/mip_heuristics/problem/problem.cu
  • cpp/src/mip_heuristics/problem/problem_helpers.cuh
  • cpp/src/mip_heuristics/solve.cu
  • cpp/src/mip_heuristics/solver.cu
  • cpp/src/mip_heuristics/utilities/cpu_worker_thread.cuh
  • cpp/src/mip_heuristics/utilities/sort_csr.cuh
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • cpp/src/pdlp/cuopt_c.cpp
  • cpp/src/pdlp/cusparse_view.cu
  • cpp/src/pdlp/cusparse_view.hpp
  • cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu
  • cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh
  • cpp/src/pdlp/optimization_problem.cu
  • cpp/src/pdlp/pdhg.cu
  • cpp/src/pdlp/pdhg.hpp
  • cpp/src/pdlp/pdlp.cu
  • cpp/src/pdlp/pdlp.cuh
  • cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu
  • cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cuh
  • cpp/src/pdlp/saddle_point.cu
  • cpp/src/pdlp/saddle_point.hpp
  • cpp/src/pdlp/solve.cu
  • cpp/src/pdlp/solve.cuh
  • cpp/src/pdlp/termination_strategy/convergence_information.cu
  • cpp/src/pdlp/termination_strategy/convergence_information.hpp
  • cpp/src/pdlp/termination_strategy/infeasibility_information.cu
  • cpp/src/pdlp/termination_strategy/termination_strategy.cu
  • cpp/src/pdlp/termination_strategy/termination_strategy.hpp
  • cpp/src/pdlp/utilities/cython_solve.cu
  • cpp/src/pdlp/utils.cuh
  • cpp/src/routing/adapters/adapted_sol.cuh
  • cpp/src/routing/structures.hpp
  • cpp/src/utilities/omp_helpers.hpp
  • cpp/src/utilities/producer_sync.hpp
  • cpp/src/utilities/work_unit_scheduler.cpp
  • cpp/src/utilities/work_unit_scheduler.hpp
  • cpp/tests/CMakeLists.txt
  • cpp/tests/dual_simplex/unit_tests/solve.cpp
  • cpp/tests/dual_simplex/unit_tests/solve_barrier.cu
  • cpp/tests/linear_programming/CMakeLists.txt
  • cpp/tests/linear_programming/grpc/grpc_integration_test.cpp
  • cpp/tests/linear_programming/mps_parser_test.cpp
  • cpp/tests/linear_programming/pdlp_test.cu
  • cpp/tests/linear_programming/unit_tests/optimization_problem_test.cu
  • cpp/tests/linear_programming/unit_tests/presolve_test.cu
  • cpp/tests/linear_programming/unit_tests/solution_interface_test.cu
  • cpp/tests/linear_programming/utilities/pdlp_test_utilities.cuh
  • cpp/tests/mip/bounds_standardization_test.cu
  • cpp/tests/mip/cuts_test.cu
  • cpp/tests/mip/determinism_test.cu
  • cpp/tests/mip/doc_example_test.cu
  • cpp/tests/mip/elim_var_remap_test.cu
  • cpp/tests/mip/feasibility_jump_tests.cu
  • cpp/tests/mip/incumbent_callback_test.cu
  • cpp/tests/mip/integer_with_real_bounds.cu
  • cpp/tests/mip/load_balancing_test.cu
  • cpp/tests/mip/mip_utils.cuh
  • cpp/tests/mip/miplib_test.cu
  • cpp/tests/mip/multi_probe_test.cu
  • cpp/tests/mip/presolve_test.cu
  • cpp/tests/mip/problem_test.cu
  • cpp/tests/mip/semi_continuous_test.cu
  • cpp/tests/mip/server_test.cu
  • cpp/tests/mip/termination_test.cu
  • cpp/tests/mip/unit_test.cu
  • cpp/tests/qp/unit_tests/no_constraints.cu
  • cpp/tests/qp/unit_tests/two_variable_test.cu
  • cpp/tests/utilities/inline_mps_test_utils.hpp
  • dependencies.yaml
  • docs/cuopt/source/conf.py
  • docs/cuopt/source/cuopt-server/client-api/sh-cli-api.rst
  • docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py
  • docs/cuopt/source/hidden/mps-api.rst
  • docs/cuopt/source/hidden/mps-example.rst
  • python/cuopt/CMakeLists.txt
  • python/cuopt/cuopt/CMakeLists.txt
  • python/cuopt/cuopt/linear_programming/CMakeLists.txt
  • python/cuopt/cuopt/linear_programming/LICENSE
  • python/cuopt/cuopt/linear_programming/README.md
  • python/cuopt/cuopt/linear_programming/__init__.py
  • python/cuopt/cuopt/linear_programming/cuopt_mps_parser/VERSION
  • python/cuopt/cuopt/linear_programming/cuopt_mps_parser/__init__.py
  • python/cuopt/cuopt/linear_programming/cuopt_mps_parser/_version.py
  • python/cuopt/cuopt/linear_programming/data_model/CMakeLists.txt
  • python/cuopt/cuopt/linear_programming/data_model/data_model.pxd
  • python/cuopt/cuopt/linear_programming/mps_parser/CMakeLists.txt
  • python/cuopt/cuopt/linear_programming/mps_parser/__init__.py
  • python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd
  • python/cuopt/cuopt/linear_programming/mps_parser/parser.py
  • python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx
  • python/cuopt/cuopt/linear_programming/mps_parser/utilities/__init__.py
  • python/cuopt/cuopt/linear_programming/mps_parser/utilities/exception_handler.py
  • python/cuopt/cuopt/linear_programming/problem.py
  • python/cuopt/cuopt/linear_programming/pyproject.toml
  • python/cuopt/cuopt/linear_programming/solver/CMakeLists.txt
  • python/cuopt/cuopt/linear_programming/solver/solver.py
  • python/cuopt/cuopt/routing/vehicle_routing.py
  • python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py
  • python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py
  • python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py
  • python/cuopt/cuopt/tests/linear_programming/test_parser.py
  • python/cuopt/pyproject.toml
  • python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py
  • python/cuopt_self_hosted/pyproject.toml
  • python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py
  • python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py
  • python/libcuopt/CMakeLists.txt
  • python/libcuopt/pyproject.toml
  • regression/benchmark_scripts/utils.py
  • skills/cuopt-developer/SKILL.md
  • skills/cuopt-developer/evals/evals.json
  • skills/cuopt-developer/resources/build_and_test.md
  • skills/cuopt-developer/resources/contributing.md
  • skills/cuopt-developer/resources/conventions.md
  • skills/cuopt-developer/resources/first_time_setup.md
  • skills/cuopt-developer/resources/python_bindings.md
  • skills/cuopt-developer/resources/troubleshooting.md
  • skills/cuopt-install/SKILL.md
  • skills/cuopt-install/evals/evals.json
  • skills/cuopt-install/resources/verification_examples.md
  • skills/cuopt-installation-api-c/SKILL.md
  • skills/cuopt-installation-api-python/SKILL.md
  • skills/cuopt-installation-api-python/resources/verification_examples.md
  • skills/cuopt-installation-common/SKILL.md
  • skills/cuopt-installation-developer/SKILL.md
  • skills/cuopt-lp-milp-api-python/assets/README.md
  • skills/cuopt-numerical-optimization-api-c/SKILL.md
  • skills/cuopt-numerical-optimization-api-c/assets/README.md
  • skills/cuopt-numerical-optimization-api-c/assets/lp_basic/README.md
  • skills/cuopt-numerical-optimization-api-c/assets/lp_basic/lp_simple.c
  • skills/cuopt-numerical-optimization-api-c/assets/lp_duals/README.md
  • skills/cuopt-numerical-optimization-api-c/assets/lp_duals/lp_duals.c
  • skills/cuopt-numerical-optimization-api-c/assets/lp_warmstart/README.md
  • skills/cuopt-numerical-optimization-api-c/assets/milp_basic/README.md
  • skills/cuopt-numerical-optimization-api-c/assets/milp_basic/milp_simple.c
  • skills/cuopt-numerical-optimization-api-c/assets/milp_production_planning/README.md

📝 Walkthrough

<review_stack_artifact>

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
cpp/src/dual_simplex/presolve.cpp (1)

1754-1769: Avoid scanning the full matrix for every corrected variable.

The nested column scan makes this postsolve step scale with the whole matrix for each bounded free variable. Converting original_problem.A to CSR once and iterating only bfv.constraint keeps the same math but avoids a large postsolve slowdown on wide problems.

♻️ Refactor sketch
   if (!presolve_info.bounded_free_variables.empty()) {
     settings.log.printf("Post-solve: Correcting duals for %d bounded free variables\n",
                         static_cast<i_t>(presolve_info.bounded_free_variables.size()));
     const csc_matrix_t<i_t, f_t>& A = original_problem.A;
+    csr_matrix_t<i_t, f_t> Arow(0, 0, 0);
+    A.to_compressed_row(Arow);
     for (const auto& bfv : presolve_info.bounded_free_variables) {
       const f_t w_j = input_z[bfv.variable];
       if (w_j == 0.0) { continue; }
       const f_t du = w_j / bfv.coefficient;
       input_y[bfv.constraint] += du;
-      for (i_t j = 0; j < A.n; j++) {
-        const i_t col_start = A.col_start[j];
-        const i_t col_end   = A.col_start[j + 1];
-        for (i_t p = col_start; p < col_end; p++) {
-          if (A.i[p] == bfv.constraint) {
-            input_z[j] -= A.x[p] * du;
-            break;
-          }
-        }
+      const i_t row_start = Arow.row_start[bfv.constraint];
+      const i_t row_end   = Arow.row_start[bfv.constraint + 1];
+      for (i_t p = row_start; p < row_end; ++p) {
+        input_z[Arow.j[p]] -= Arow.x[p] * du;
       }
     }
   }

As per coding guidelines, "Assess algorithmic complexity for large-scale problems (millions of variables/constraints); ensure O(n log n) or better, not O(n²) or worse."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/dual_simplex/presolve.cpp` around lines 1754 - 1769, The loop
currently scans every column of original_problem.A (CSC) for each
presolve_info.bounded_free_variables entry which is O(n²); convert or provide a
CSR view of original_problem.A once (e.g., build a row-oriented structure like
A_csr or a helper method getRow(constraint)) and then, inside the
bounded_free_variables loop (referencing input_z, input_y, bfv.constraint,
bfv.coefficient), use the CSR row for bfv.constraint to update input_z[j]
directly for only the nonzeros in that row and avoid the inner column scan;
ensure the CSR conversion is done once before the loop and reuse it for all bfv
entries to preserve numerical results while reducing complexity.
cpp/tests/dual_simplex/unit_tests/solve_barrier.cu (1)

184-217: Add the upper-bound / negated-variable polarity as a sibling case.

This only covers a formerly-free variable that ends up with a retained lower implied bound. The new code also has separate upper-bound bookkeeping and then routes those variables through the negated_variables round-trip in cpp/src/dual_simplex/presolve.cpp, so that branch is still untested here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/tests/dual_simplex/unit_tests/solve_barrier.cu` around lines 184 - 217,
The test only exercises the case where a formerly-free variable receives a
retained lower implied bound; add a sibling test that triggers the
upper-bound/negated-variable path so presolve's upper-bound bookkeeping and the
negated_variables round-trip in cpp/src/dual_simplex/presolve.cpp are exercised.
Copy the existing TEST(barrier, min_x_squared_free_variable_dual_correction)
logic into a new TEST (e.g., min_x_squared_free_variable_negated_polarity), use
a problem variant (or modify the MPS) that forces an implied upper bound on the
free variable (so solve_lp and parse_mps are called with that MPS), call
cuopt::linear_programming::solve_lp and the same host_copy/EXPECT checks but
assert the expected primal/dual/reduced-cost values for the negated-variable
outcome, and ensure the new test references the same setup helpers
(get_rapids_dataset_root_dir, parse_mps, solve_lp) so the negated_variables
branch is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/src/dual_simplex/presolve.cpp`:
- Around line 1751-1759: The loop updating input_y for
presolve_info.bounded_free_variables divides by bfv.coefficient and only skips
when w_j == 0.0, which is numerically unsafe; change the guard to check for
near-zero values using an epsilon (e.g., fabs(bfv.coefficient) < eps or
fabs(w_j) < eps) and skip or clamp the update when either the coefficient or w_j
is too small to avoid huge du; update the conditional around computing du
(referencing presolve_info.bounded_free_variables, bfv.coefficient, input_z,
input_y) to use an absolute epsilon comparison and optionally log or count
skipped tiny-coefficient cases instead of performing the division.

---

Nitpick comments:
In `@cpp/src/dual_simplex/presolve.cpp`:
- Around line 1754-1769: The loop currently scans every column of
original_problem.A (CSC) for each presolve_info.bounded_free_variables entry
which is O(n²); convert or provide a CSR view of original_problem.A once (e.g.,
build a row-oriented structure like A_csr or a helper method getRow(constraint))
and then, inside the bounded_free_variables loop (referencing input_z, input_y,
bfv.constraint, bfv.coefficient), use the CSR row for bfv.constraint to update
input_z[j] directly for only the nonzeros in that row and avoid the inner column
scan; ensure the CSR conversion is done once before the loop and reuse it for
all bfv entries to preserve numerical results while reducing complexity.

In `@cpp/tests/dual_simplex/unit_tests/solve_barrier.cu`:
- Around line 184-217: The test only exercises the case where a formerly-free
variable receives a retained lower implied bound; add a sibling test that
triggers the upper-bound/negated-variable path so presolve's upper-bound
bookkeeping and the negated_variables round-trip in
cpp/src/dual_simplex/presolve.cpp are exercised. Copy the existing TEST(barrier,
min_x_squared_free_variable_dual_correction) logic into a new TEST (e.g.,
min_x_squared_free_variable_negated_polarity), use a problem variant (or modify
the MPS) that forces an implied upper bound on the free variable (so solve_lp
and parse_mps are called with that MPS), call
cuopt::linear_programming::solve_lp and the same host_copy/EXPECT checks but
assert the expected primal/dual/reduced-cost values for the negated-variable
outcome, and ensure the new test references the same setup helpers
(get_rapids_dataset_root_dir, parse_mps, solve_lp) so the negated_variables
branch is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c207bc0d-0074-4238-af72-79a21036cba5

📥 Commits

Reviewing files that changed from the base of the PR and between b991799 and f540331.

📒 Files selected for processing (6)
  • cpp/src/dual_simplex/presolve.cpp
  • cpp/src/dual_simplex/presolve.hpp
  • cpp/src/dual_simplex/solve.cpp
  • cpp/tests/dual_simplex/unit_tests/solve.cpp
  • cpp/tests/dual_simplex/unit_tests/solve_barrier.cu
  • datasets/quadratic_programming/min_x_squared.mps

Comment on lines +1751 to +1759
if (!presolve_info.bounded_free_variables.empty()) {
settings.log.printf("Post-solve: Correcting duals for %d bounded free variables\n",
static_cast<i_t>(presolve_info.bounded_free_variables.size()));
const csc_matrix_t<i_t, f_t>& A = original_problem.A;
for (const auto& bfv : presolve_info.bounded_free_variables) {
const f_t w_j = input_z[bfv.variable];
if (w_j == 0.0) { continue; }
const f_t du = w_j / bfv.coefficient;
input_y[bfv.constraint] += du;
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard the dual update against tiny a_{i*,j} and tiny w_j.

This divides by bfv.coefficient directly and only skips exact-zero w_j. On ill-conditioned rows, a near-zero coefficient will blow up du, and exact floating-point equality here is too strict for postsolve residuals.

As per coding guidelines, "Check numerical stability: prevent overflow/underflow, precision loss, division by zero/near-zero, and use epsilon comparisons for floating-point equality checks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/dual_simplex/presolve.cpp` around lines 1751 - 1759, The loop
updating input_y for presolve_info.bounded_free_variables divides by
bfv.coefficient and only skips when w_j == 0.0, which is numerically unsafe;
change the guard to check for near-zero values using an epsilon (e.g.,
fabs(bfv.coefficient) < eps or fabs(w_j) < eps) and skip or clamp the update
when either the coefficient or w_j is too small to avoid huge du; update the
conditional around computing du (referencing
presolve_info.bounded_free_variables, bfv.coefficient, input_z, input_y) to use
an absolute epsilon comparison and optionally log or count skipped
tiny-coefficient cases instead of performing the division.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chris-maes is this a good suggestion ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We remove small coefficients in A in presolve. I'm not sure if this is necessary.

@Iroy30
Copy link
Copy Markdown
Member

Iroy30 commented Apr 22, 2026

closes #1119

@chris-maes
Copy link
Copy Markdown
Contributor Author

/ok to test c29cc5e

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cpp/src/dual_simplex/presolve.hpp (1)

132-139: Clarify index-space/sign semantics in bounded_free_var_t docs.

Please document whether variable/constraint are indices in the original LP space and whether coefficient is stored before or after any presolve sign transformations. That contract is easy to misread later and is central to the dual correction logic.

Suggested doc tweak
 struct bounded_free_var_t {
-  i_t variable;    // j: the originally-free variable
-  i_t constraint;  // i*: the constraint that implied the bound
-  f_t coefficient; // a_{i*,j}: the coefficient of x_j in constraint i*
+  i_t variable;    // j in original problem indexing: originally-free variable
+  i_t constraint;  // i* in original problem indexing: constraint that implied the bound
+  f_t coefficient; // a_{i*,j} used by dual correction (document transformed/original sign convention)
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/dual_simplex/presolve.hpp` around lines 132 - 139, Clarify the
contract for bounded_free_var_t by updating its doc comment: state that variable
and constraint are indices in the original (pre-presolve) LP index space (i.e.,
they refer to original variable j and original constraint i*), and specify that
coefficient stores the original matrix value a_{i*,j} prior to any sign or
scaling transformations performed during presolve so uncrush/dual correction can
apply the same presolve sign logic; reference bounded_free_var_t and its fields
variable, constraint, and coefficient in the comment so future readers know
these are original indices and the coefficient is the pre-presolve value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cpp/src/dual_simplex/presolve.hpp`:
- Around line 132-139: Clarify the contract for bounded_free_var_t by updating
its doc comment: state that variable and constraint are indices in the original
(pre-presolve) LP index space (i.e., they refer to original variable j and
original constraint i*), and specify that coefficient stores the original matrix
value a_{i*,j} prior to any sign or scaling transformations performed during
presolve so uncrush/dual correction can apply the same presolve sign logic;
reference bounded_free_var_t and its fields variable, constraint, and
coefficient in the comment so future readers know these are original indices and
the coefficient is the pre-presolve value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: efb61bb2-7b75-4b86-8b3d-f11088198164

📥 Commits

Reviewing files that changed from the base of the PR and between f540331 and c29cc5e.

📒 Files selected for processing (1)
  • cpp/src/dual_simplex/presolve.hpp

@chris-maes
Copy link
Copy Markdown
Contributor Author

/ok to test 3ee78ee

@github-actions
Copy link
Copy Markdown

🔔 Hi @anandhkb @chris-maes, this pull request has had no activity for 7 days. Please update or let us know if it can be closed. Thank you!

If this is an "epic" issue, then please add the "epic" label to this issue.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.

rgsl888prabhu and others added 6 commits May 18, 2026 14:02
## Summary

Initial skill evaluation dataset for `cuopt-lp-milp-api-python` at `skills/cuopt-lp-milp-api-python/evals/evals.json`. 99 entries adapted from the [microsoft/OptiGuide](https://github.com/microsoft/OptiGuide) IndustryOR corpus (MIT, attribution in `evals/SOURCES.md`).

- `ground_truth` is the numeric optimal value; rubric requires exact match to the precision shown (no tolerance)
- `expected_behavior` is generic across all entries — does not pre-categorize as LP vs MILP
- Each entry has a `source` field referencing the dataset row for traceability

QP eval set is out of scope (the corpus has no genuine QP problems) and will follow in a separate PR.

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Ishika Roy (https://github.com/Iroy30)

URL: NVIDIA#1172
…1182)

## Summary
- Declare `CUOPT_SLACK_MENTION_ID` as an optional `workflow_call` secret
on the reusable `nightly-summary` workflow and forward it to the
run-step env.
- Pass it from `test.yaml` to the `nightly-summary` job alongside the
other Slack secrets.

## Why
`ci/nightly_summary.sh` already forwards `CUOPT_SLACK_MENTION_ID` to
`ci/utils/generate_slack_payloads.py`, which uses it to prefix `<@id>`
mentions on **new failures** and **new flaky tests**. Until now the env
var was never populated in CI, so the mention was always empty. With
this change, setting the `CUOPT_SLACK_MENTION_ID` repo secret (Slack
user ID like `U01ABCDEF` or group handle like `S01ABCDEF`) will ping the
configured handle. Leaving it unset preserves the no-mention default.

## Test plan
- [ ] Add `CUOPT_SLACK_MENTION_ID` repo secret.
- [ ] Trigger a nightly run (or `workflow_dispatch` on
`nightly-summary`) and confirm the Slack message includes the `<@id>`
mention on new failures.
- [ ] Verify behavior is unchanged when the secret is unset (no
mention).
…DIA#1020)

This PR simplify the pseudo cost class in such a way that both regular and deterministic B&B use the same code path as much as possible. It also disable mutexes and atomics when running in deterministic mode as each thread has its own snapshot of the pseudocost. It also move the routines related with the diving heuristics back to the `diving_heuristics.cpp`, renamed `branch_and_bound_worker.hpp` to `worker.hpp` to match the new file structure and moved `worker_pool_t` to a dedicated header.


Regular mode (GH200, 10min):
```
================================================================================
 main-190326-2 (1) vs simplify-pseudocost (2)
================================================================================

------------------------------------------------------------------------------------------------------------------------------
|                                        |       Run 1        |       Run 2        |     Abs. Diff.     |   Rel. Diff. (%)   |
------------------------------------------------------------------------------------------------------------------------------
| Feasible                                                 226                  226                   +0                 --- |
| Optimal                                                   70                   67                   -3                 --- |
| Solutions with <0.1% primal gap                          121                  122                   +1                 --- |
| Nodes explored (mean)                           4283972.9121         4455377.8117         +171404.8996              +3.847 |
| Nodes explored (shifted geomean)                   6202.3471            7062.2682            +859.9210             +12.176 |
| Relative MIP gap (mean)                               0.3382               0.3337              -0.0045              -1.325 |
| Relative MIP gap (shifted geomean)                    0.1193               0.1166              -0.0027              -2.293 |
| Solve time (mean)                                   450.2347             452.9154              +2.6806              +0.592 |
| Solve time (shifted geomean)                        221.4772             227.6381              +6.1609              +2.706 |
| Primal gap (mean)                                    11.4459              11.0482              -0.3976              -3.474 |
| Primal gap (shifted geomean)                          0.6591               0.6008              -0.0582              -8.838 |
| Primal integral (mean)                               49.9109              54.6941              +4.7832              +8.745 |
| Primal integral (shifted geomean)                    11.5672              13.9826              +2.4153             +17.274 |
------------------------------------------------------------------------------------------------------------------------------
```

Determinism mode (GH200, 5min):
```
================================================================================
 main-240426-determinism (1) vs simplify-pseudocost-determinism (2)
================================================================================

------------------------------------------------------------------------------------------------------------------------------
|                                        |       Run 1        |       Run 2        |     Abs. Diff.     |   Rel. Diff. (%)   |
------------------------------------------------------------------------------------------------------------------------------
| Feasible                                                 179                  179                   +0                 --- |
| Optimal                                                   45                   46                   +1                 --- |
| Solutions with <0.1% primal gap                           64                   64                   +0                 --- |
| Nodes explored (mean)                              1.556e+06            1.511e+06           -4.526e+04               -2.91 |
| Nodes explored (shifted geomean)                        1895                 1900               +5.427              +0.286 |
| Relative MIP gap (mean)                                7.038               0.7827               -6.255               -88.9 |
| Relative MIP gap (shifted geomean)                    0.2039               0.1841             -0.01984               -9.73 |
| Solve time (mean)                                      249.5                251.7               +2.153              +0.855 |
| Solve time (shifted geomean)                           153.8                160.6               +6.767               +4.22 |
| Primal gap (mean)                                       39.8                39.43              -0.3723              -0.935 |
| Primal gap (shifted geomean)                           5.315                5.314            -0.001059             -0.0199 |
| Primal integral (mean)                                   292                299.2               +7.201               +2.41 |
| Primal integral (shifted geomean)                      49.42                50.91               +1.486               +2.92 |
------------------------------------------------------------------------------------------------------------------------------
```

Authors:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

Approvers:
  - Alice Boucher (https://github.com/aliceb-nv)
  - Trevor McKay (https://github.com/tmckayus)
  - Chris Maes (https://github.com/chris-maes)

URL: NVIDIA#1020
## Summary

Make the two matrix entries in `.github/workflows/nightly.yaml` independent so a missing release-branch ref no longer cancels the working `main`-branch nightly.

**Why this matters now:** the matrix bumped to `release/26.06` in NVIDIA#1091 on 2026-04-14, but `release/26.06` has not been cut from `main` yet. Every nightly since has failed — the release entry 404s at the `gh api .../branches/release/26.06` lookup, the matrix's default `fail-fast: true` cancels the sibling `main` entry, and the wrapper job reports `failure` even though the `main` dispatch would have succeeded on its own.

**Fix:** add `fail-fast: false` so each cuopt_branch dispatch runs independently.

```yaml
strategy:
  fail-fast: false       # ← added
  matrix:
    cuopt_branch:
      - "main"
      - "release/26.06"
```

The release entry will still go red until `release/26.06` is actually cut (or the line is reverted to a release branch that exists), but it no longer drags `main` with it.

## Test plan
- [ ] Watch the next scheduled nightly — `main` matrix entry should dispatch and complete independent of the `release/26.06` entry's outcome.
- [ ] Manual dispatch via `Run workflow` from Actions UI to verify both entries run to completion (or independent failure) before waiting for the cron.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - James Lamb (https://github.com/jameslamb)

URL: NVIDIA#1186
…family (NVIDIA#1183)

## Summary

Collapses the LP/MILP and QP skills into a single
**numerical-optimization** skill family across all surfaces (formulation
+ Python / C / CLI APIs). cuOpt's solver API is shared across LP, MILP,
and QP — keeping per-problem-type skills with overlapping triggers just
creates an activation-routing problem for the agent.

Two commits on this branch:

1. **`0bf8ad4b`** — Combine `lp-milp-formulation` and `qp-formulation`
into `numerical-optimization-formulation` (concepts + parsing +
patterns).
2. **`af70748c`** — Combine the per-interface API skills:
- `cuopt-lp-milp-api-python` + `cuopt-qp-api-python` →
`cuopt-numerical-optimization-api-python`
- `cuopt-lp-milp-api-c` + `cuopt-qp-api-c` →
`cuopt-numerical-optimization-api-c`
- `cuopt-lp-milp-api-cli` + `cuopt-qp-api-cli` →
`cuopt-numerical-optimization-api-cli`

Renames go through `git mv` so history (and the existing `evals/`
dataset) is preserved. QP-specific assets and the QP examples doc were
folded into the renamed Python skill (`resources/qp_examples.md`); QP C
and CLI had no standalone source files, so their content became small
sub-sections in the merged SKILL.md.

Cross-refs updated: `AGENTS.md` (also covers `.claude/CLAUDE.md` via
symlink), `.claude-plugin/marketplace.json`,
`skills/skill-evolution/SKILL.md`, all 99 `expected_skill` values in the
Python evals dataset, and `evals/SOURCES.md`.

Per-skill SKILL.md additions for QP coverage (Python: decision table,
portfolio example, MINIMIZE-only / continuous-only / Q-PSD rules, status
note, two new Common Issues rows; C: QP-via-C-API section; CLI:
QP-via-CLI section).

## Test plan
- [x] `ci/utils/validate_skills.sh` passes
- [x] `pre-commit run` passes (all hooks: end-of-files, trailing
whitespace, JSON, ruff, copyright, hardcoded version,
sync-skills-version, validate-agent-skills)
- [x] Merged Python SKILL.md ~278 lines, below the resources/ split
threshold
- [x] No remaining references to `cuopt-{lp-milp,qp}-api-{python,c,cli}`
anywhere in the tree
- [ ] Verify CI passes

---------

Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
…VIDIA#1187)

## Summary
- Detect user-group (subteam) IDs by their `S` prefix and emit
`<!subteam^S...>` syntax.
- User IDs (prefix `U`/`W`) continue to use `<@U...>`.
- Update the contract comment to spell out both forms and warn that
handle names alone will not ping.

## Why
Today's nightly Slack post (2026-05-07) showed `@cuopt-ci-team` rendered
as plain text — the message arrived but produced no notification. Root
cause: `ci/utils/generate_slack_payloads.py` always wrapped
`CUOPT_SLACK_MENTION_ID` as `<@id>`, which is **user-only** syntax.
Slack requires `<!subteam^SXXXXXXXX>` to ping a user group. Passing the
group's handle name (`cuopt-ci-team`) or its `S...` ID with the old
formatter both fail silently.

## Test plan
- [ ] `CUOPT_SLACK_MENTION_ID=SXXXXXXXX` → next nightly with new
failures pings the `cuopt-ci-team` group.
- [ ] Sanity-check that a user ID (`U...`) still renders as `<@U...>`
(no behavior change for that path).
- [ ] Empty/unset `CUOPT_SLACK_MENTION_ID` produces no mention (existing
behavior preserved).

## Notes
Builds on NVIDIA#1182, which plumbs the `CUOPT_SLACK_MENTION_ID` secret
through the workflow. Either order of merge works; this PR only touches
the formatter.
Kh4ster and others added 26 commits May 18, 2026 14:02
This PR greatly extends the capabilities of batch PDLP. Former batch PDLP only supported having a single variable bounds being different per climber. It now supports:
- Different constraints lower and upper bounds per climber
- Different objective coefficients per climber
- Different objective offset per climber
- More than one variable bound difference per climber

This PR also adds the support of per climber residual and first primal feasible to the Stable3 PDLP solver mode and its batch version. It allows to solve a batch of problems and stop once one or all the climbers have reached primal feasibility.

All those combinations can be put together, resulting in a potential:

Solve a batch of LPs all having different: constraints lower and upper bounds, objective coefficients, objective offset, variable bounds, using per constraint residual instead of the L2 norm and stopping once one, or all, climbers have reached primal feasibility.

Authors:
  - Nicolas Blin (https://github.com/Kh4ster)
  - Trevor McKay (https://github.com/tmckayus)

Approvers:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)
  - Trevor McKay (https://github.com/tmckayus)

URL: NVIDIA#1152
…IA#1176)

## Summary

Iterative refinement of the `cuopt-developer` skill driven by `astra-skill-eval` (NV-ACES) runs against its eval dataset.

**SKILL.md content & structure**
- Sharpened description and added a **Pre-flight Checks** block (CUDA driver compatibility, conda-env activation, `PARALLEL_LEVEL`, dataset pointer) at the top of *Build & Test*.
- **Refusal Rules — Read First** moved to the top with literal scripts for the five categories that surfaced silent compliance in eval runs (package installs, CI bypass, outside-workspace writes, destructive commands, sudo). Refusals are absolute — no "with approval" escape (per CodeRabbit review).
- **Compartmentalized into `resources/`**: `build_and_test.md`, `contributing.md`, `conventions.md`, `troubleshooting.md`. SKILL.md drops from ~4400 → ~1500 tokens.

**Sibling-skill scoping (eval routing fix)**
- `cuopt-user-rules` scoped to end users only (no longer competes on dev prompts).
- `cuopt-installation-developer` **folded into `cuopt-developer`** as `resources/first_time_setup.md` after the install skill collapsed to ~30 lines once duplication was squeezed out (CUDA check + build/test commands already lived in cuopt-developer). 10 `inst-*` evals migrated into `cuopt-developer/evals/evals.json` (40 → 50, IDs preserved for provenance). Eliminates the routing collision the eval runs flagged as "borderline competitor on raw 'build from source' prompts".

### Eval impact
- Astra Layer 1 static check: **78 → 84** (Grade C → B); large-skill warning cleared after compartmentalization.
- Astra Harbor (opencode, group-mode skill-lift): aggregate with-skill score **0.62 → 0.80**; routing collisions on `cuopt-user-rules` eliminated.

### Out of scope
- Four safety-refusal cases (`dev-006/021/025/037`) still fail in opencode runs because opencode often does not load `cuopt-developer` at all on those prompts — the new Refusal Rules block never reaches the model. Agent-side characteristic; tracked as a known issue.
- End-to-end Harbor BYOT task (real `./build.sh` + `ctest`) deferred to a separate branch (needs GPU sandbox + custom Dockerfile + verifier scripts).

## Issue
NA

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)
  - Ishika Roy (https://github.com/Iroy30)

Approvers:
  - Ishika Roy (https://github.com/Iroy30)

URL: NVIDIA#1176
## Summary

Collapses three user-facing install skills into one **`cuopt-install`** skill covering Python, C, and server across pip, conda, and Docker.

| Old | New |
|---|---|
| `cuopt-installation-common` (concepts) | merged → `cuopt-install` |
| `cuopt-installation-api-python` | merged → `cuopt-install` |
| `cuopt-installation-api-c` | merged → `cuopt-install` |
| `cuopt-installation-developer` (build from source) | unchanged here — being folded into `cuopt-developer` in NVIDIA#1176 |

Two commits:
1. **Consolidation** — collapse the three user-install skills into a single `cuopt-installation` skill.
2. **Rename** — `cuopt-installation` → `cuopt-install` (shorter, matches `pip install` mental model, removes residual audience ambiguity).

## Why

- System requirements, package-manager decisions, and verification surface are **shared across interfaces**; splitting them created duplicated content and ambiguous activation when an agent doesn't yet know which interface the user wants.
- Both API skills already declared **"Standalone; no common skill"** — the common skill was vestigial.
- Mirrors the consolidation pattern from NVIDIA#1183 (numerical-optimization skill family).
- Renaming to `cuopt-install` follows the `pip install` convention; the developer / build-from-source path lives elsewhere (`cuopt-developer`, post-NVIDIA#1176).

## Drive-by fixes (caught while drafting)

- **C API was missing pip support.** `libcuopt-cu12` and `libcuopt-cu13` are real PyPI packages (see `dependencies.yaml:436,441`); the old `cuopt-installation-api-c` skill only documented conda.
- **C API conda command was wrong.** Old skill said `conda install ... cuopt`, but `cuopt` is the **Python** conda package. The C package is `libcuopt` (`dependencies.yaml:423`).

## Cross-refs updated

- `AGENTS.md` — skill index reorganized; new "Installation" section with `cuopt-install`.
- `.claude-plugin/marketplace.json` — three plugin entries → one.
- `skills/cuopt-installation-developer/evals/evals.json` — two references in `ground_truth` / `expected_behavior` updated to `cuopt-install`.

## Coordination with NVIDIA#1176

Both PRs touch `AGENTS.md`, `.claude-plugin/marketplace.json`, and `skills/cuopt-installation-developer/evals/evals.json`. Whichever lands second will need a small rebase (drop the eval edit since NVIDIA#1176 deletes that file; flip the "Installation" section in AGENTS.md). The merged eval cases that NVIDIA#1176 migrates into `cuopt-developer/evals/evals.json` may also need a one-line touch-up to point at `cuopt-install` rather than the old skill names.

## Test plan

- [ ] Skill registry loads — `cuopt-install` appears in available skills, the three old user-install names do not.
- [ ] User asks "how do I install cuOpt for Python?" → activates `cuopt-install`, produces correct pip / conda command.
- [ ] User asks "how do I install cuOpt for C?" → activates `cuopt-install`, produces correct `libcuopt-cu12/13` pip command and `libcuopt` conda command.
- [ ] User asks "how do I build cuOpt from source?" → still activates `cuopt-installation-developer` (or `cuopt-developer` post-NVIDIA#1176).

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Ishika Roy (https://github.com/Iroy30)

URL: NVIDIA#1189
I just saw a case where the agent refused to run the standard build process because build.sh installs cuopt packages into the python environment. Plus, there are autorun environments where we do want the agent to install packages autonomously. This type of instruction doesn't belong at the individual package level.

Authors:
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

URL: NVIDIA#1200
…NVIDIA#1181)

Similar to upstream changes in `shared-workflows`, this PR cleans up and annotates all of the workflows and adds the `zizmor` linter to make sure changes are checked.

Part of rapidsai/build-planning#275

Authors:
  - Gil Forsyth (https://github.com/gforsyth)
  - https://github.com/jakirkham
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Ramakrishnap (https://github.com/rgsl888prabhu)
  - https://github.com/jakirkham

URL: NVIDIA#1181
EXPECT_DOUBLE_EQ was observed failing in CI with a 2.2e-15 error:

https://github.com/NVIDIA/cuopt/actions/runs/25695199349/job/75445965119

```
Expected equality of these values:
  solution.get_objective_value()
    Which is: 2.9999999999999978
  3.0
    Which is: 3
```

Authors:
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

URL: NVIDIA#1199
…thon, thirdparty) (NVIDIA#1191)

## Summary

When a test runner — gtest binary or pytest — is killed by a signal mid-run, it doesn't finalize its `--gtest_output=xml:` / `--junitxml` output. `ci/utils/nightly_report.py` classifies tests purely from those XML files, so a mid-run crash with no other captured failures was reported as **"All tests passed."** while the script exited non-zero. The GitHub step was correctly red, but the textual summary lied.

Observed in a recent cpp run: `PDLP_TEST` reported five `[ FAILED ]` cases and then segfaulted, but the structured summary said `Classification: 459 passed, 0 failed, 0 errors, 0 flaky, 0 skipped` and concluded `All tests passed.`

## Changes

### Crash-XML synthesis (the actual fix)
- `ci/utils/crash_helpers.sh` — add `write_pytest_crash_marker` helper that writes `<junitxml>-crash.xml` on pytest signal death, alongside (not over) any partial XML pytest may have emitted.
- `ci/run_ctests.sh` — non-nightly crash path now calls `write_crash_xml` for the gtest binary; tracks failed binaries in a `FAILED_BINARIES` array and prints them at end of run.
- `ci/run_cuopt_pytests.sh`, `ci/run_cuopt_server_pytests.sh` — call `write_pytest_crash_marker` on the non-nightly crash path.
- `ci/thirdparty-testing/run_pulp_tests.sh`, `run_pyomo_tests.sh`, `run_cvxpy_tests.sh` — capture pytest's exit code and synthesize a crash XML on signal death.

### Per-step failure summaries
- `ci/test_cpp.sh`, `ci/test_python.sh`, `ci/test_wheel_cuopt.sh`, `ci/test_wheel_cuopt_server.sh` — track each major step in a `FAILED_STEPS` array via `|| FAILED_STEPS+=(...)` and print a `==== FAILED TEST STEPS ====` block before the final `exit ${EXITCODE}`.

### Out of scope
`test_notebooks.sh` and `test_doc_examples.sh` don't produce JUnit XML and exit non-zero on the first failure (notebooks via `exit 1`, doc-examples via its own EXIT-trap counter), so they don't have the same false-pass mode.

## Why this shape
- Crash XMLs go to a separate `*-crash.xml` filename so we never clobber a partial XML emitted by gtest/pytest before the crash.
- The nightly retry path is untouched — it already calls `write_crash_xml` per retry attempt and via `pytest_crash_isolate`. This PR plugs the gap on the non-nightly path (PR runs and any non-`nightly` `RAPIDS_BUILD_TYPE`).
- Step summaries print to stdout only (per maintainer preference). `nightly_report.py` continues to write the structured markdown / HTML / GITHUB_STEP_SUMMARY.

## Test plan

- [ ] On a deliberately-crashing gtest binary, verify `<test_name>-crash.xml` is written and `nightly_report.py` lists it as a failure.
- [ ] On a deliberately-crashing pytest invocation, verify `<junit-name>-crash.xml` is written and the failure surfaces in the report.
- [ ] On a normal (non-crash) test failure, verify behavior is unchanged.
- [ ] On a fully-passing run, verify no `FAILED gtest BINARIES` / `FAILED TEST STEPS` block is printed.
- [ ] `pre-commit run --files ci/...` passes (shellcheck wired in).

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Bradley Dice (https://github.com/bdice)

URL: NVIDIA#1191
## Summary

- Adds `skills/cuopt-install/evals/evals.json` with 15 behavior-style evals for the `cuopt-install` skill. Coverage: required questions, Python/C/server install (pip/conda/Docker), CUDA suffix matching, verification, the mandatory no-auto-install rule, GPU Compute Capability requirements, and the redirect to `cuopt-developer` for from-source builds.
- Corrects a factual error in `cuopt-install/SKILL.md` (3 lines) and `cuopt-user-rules/SKILL.md` (table + note): `cuopt-cuXX` declares `libcuopt-cuXX` as a runtime dependency (see `python/cuopt/pyproject.toml:26`), so the C library and headers are already installed alongside the Python package. The skills previously stated the Python and C packages were strictly separate; the reverse (`libcuopt` alone, no Python) is the only true standalone case.

`cuopt-install` was the only user-facing skill without an evals directory; this closes that gap. `cuopt-user-rules` has no evals (rules skill, not API), so no eval changes needed there.

Style of the new evals matches `cuopt-developer/evals/evals.json` (question + ground-truth narrative + `expected_behavior` list).

## Test plan

- [ ] JSON validates (`python -m json.tool skills/cuopt-install/evals/evals.json`)
- [ ] Eval IDs are unique within the file
- [ ] Eval harness picks up the new file
- [ ] Spot-check install-005 against the corrected SKILL.md / user-rules guidance

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Trevor McKay (https://github.com/tmckayus)

URL: NVIDIA#1205
Join concurrent solver worker threads before rethrowing exceptions so std::thread destructors do not terminate the process. Add a deterministic regression test that exercises a PDLP validation error after concurrent workers start.

Prevents some core dumps and gives a more useful error message for the test_incumbent_callbacks flaky test failure.

Old failure:
```
 | Explored | Unexplored |    Objective    |     Bound     | IntInf | Depth | Iter/Node |   Gap    |  Time  |
terminate called without an active exception
Fatal Python error: Aborted

Current thread 0x0000e727cfdde020 (most recent call first):
  File "/pyenv/versions/3.11.15/lib/python3.11/site-packages/cuopt/linear_programming/solver/solver.py", line 98 in Solve
  File "/pyenv/versions/3.11.15/lib/python3.11/site-packages/cuopt/utilities/exception_handler.py", line 24 in func
  File "/__w/cuopt/cuopt/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py", line 87 in _run_incumbent_solver_callback
  File "/__w/cuopt/cuopt/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py", line 112 in test_incumbent_get_callback
```

New failure:
```
=================================== FAILURES ===================================
_________________ test_incumbent_get_callback[/mip/swath1.mps] _________________

file_name = '/mip/swath1.mps'

    @pytest.mark.parametrize(
        "file_name",
        [
            ("/mip/swath1.mps"),
            ("/mip/neos5-free-bound.mps"),
        ],
    )
    def test_incumbent_get_callback(file_name):
>       _run_incumbent_solver_callback(file_name, include_set_callback=False)

tests/linear_programming/test_incumbent_callbacks.py:112: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tests/linear_programming/test_incumbent_callbacks.py:87: in _run_incumbent_solver_callback
    solution = solver.Solve(data_model_obj, settings)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/conda/envs/test/lib/python3.12/site-packages/cuopt/utilities/exception_handler.py:48: in func
    raise e
/opt/conda/envs/test/lib/python3.12/site-packages/cuopt/utilities/exception_handler.py:24: in func
    return f(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^
/opt/conda/envs/test/lib/python3.12/site-packages/cuopt/linear_programming/solver/solver.py:98: in Solve
    s = solver_wrapper.Solve(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   ???
E   RuntimeError: CUDA error encountered at: file=/tmp/conda-bld-output/bld/rattler-build_libmps-parser/work/cpp/src/pdlp/utilities/ping_pong_graph.cu line=57: call='cudaStreamEndCapture(stream_view_.value(), &even_graph)', Reason=cudaErrorStreamCaptureInvalidated:operation failed due to a previous error during capture
```

Authors:
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Hugo Linsenmaier (https://github.com/hlinsen)

URL: NVIDIA#1206
After the OpenMP transition (NVIDIA#1099), the determinism mode uses a nested `omp parallel` regiom, which are disabled by default. Hence, the determinism mode is only using a single thread in the current implementation. This PR fixes that.

Authors:
  - Nicolas L. Guidotti (https://github.com/nguidotti)

Approvers:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

URL: NVIDIA#1201
## Issue

Authors:
  - Alice Boucher (https://github.com/aliceb-nv)
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)

Approvers:
  - Akif ÇÖRDÜK (https://github.com/akifcorduk)
  - Nicolas L. Guidotti (https://github.com/nguidotti)

URL: NVIDIA#1179
Opened NVIDIA#1207 to track the fix.

Authors:
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Ramakrishnap (https://github.com/rgsl888prabhu)
  - Nicolas L. Guidotti (https://github.com/nguidotti)

URL: NVIDIA#1208
This PR removes most of the unused/one-time-used device_vectors in cuPDLPx to reduce the memory footprint. It allows to run bigger problems without running out of memory

Authors:
  -  Bulle Mostovoi (https://github.com/Bubullzz)

Approvers:
  - Miles Lubin (https://github.com/mlubin)
  - Nicolas Blin (https://github.com/Kh4ster)
  - Ramakrishnap (https://github.com/rgsl888prabhu)

URL: NVIDIA#1153
## Summary
- install only the Boost components needed by cuOpt's PaPILO dependency on Ubuntu
- add `libboost-iostreams-dev` and `libboost-serialization-dev` explicitly rather than `libboost-dev`
- skip `libboost-program-options-dev` -- this is optional and not needed for PaPILO with the options we provide (`PAPILO_NO_BINARIES=ON`)

Authors:
  - Bradley Dice (https://github.com/bdice)
  - James Lamb (https://github.com/jameslamb)
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - James Lamb (https://github.com/jameslamb)

URL: NVIDIA#1165
early_cpufj and early_gpufj capture early_best_objective, early_best_user_obj, early_best_user_assignment, and early_callback_mutex by reference via the callbacks. They should be destructed before the values they capture by reference for safety.

Authors:
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Rajesh Gandham (https://github.com/rg20)

URL: NVIDIA#1216
early_fj_start is a local variable and should be captured by value since the callback outlives the local scope.

Authors:
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Rajesh Gandham (https://github.com/rg20)

URL: NVIDIA#1214
### Summary
- Fix typo in NodeInfo location bound assert message in cpp/src/routing/structures.hpp.
- Update to 32768 to match the existing check location < (1 << 15).
- No behavior change: message correction for debugging clarity.

Authors:
  - Daniel (https://github.com/aycsi)
  - Miles Lubin (https://github.com/mlubin)

Approvers:
  - Miles Lubin (https://github.com/mlubin)
  - Rajesh Gandham (https://github.com/rg20)

URL: NVIDIA#1218
## Summary
- The concurrency group in `build.yaml` was keyed on `workflow + ref`, so a push to `main` (or `release/*`) would cancel an in-progress nightly-dispatched `build.yaml` run on the same branch — and the reverse was equally true.
- Add the `build_type` input to the group key (falling back to `'branch'` when `inputs` is unset on `push`) so nightly and branch builds occupy separate groups.
- Same-type, same-ref runs still cancel older runs (e.g. rapid pushes to `main` still supersede each other).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - James Lamb (https://github.com/jameslamb)

URL: NVIDIA#1211
Contributes to rapidsai/build-planning#265

Closes NVIDIA#1155.

* uses CUDA 13.2.0 to build and test
* updates to CUDA 13.2.0 devcontainers

## Notes for Reviewers

This switches GitHub Actions workflows to the `cuda-13.2.0` branch from here: rapidsai/shared-workflows#545

A future round of PRs will revert that back to `main`, once all of RAPIDS is migrated.

Authors:
  - Bradley Dice (https://github.com/bdice)
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - James Lamb (https://github.com/jameslamb)
  - Ramakrishnap (https://github.com/rgsl888prabhu)

URL: NVIDIA#1198
Boosts performance when many problems are scheduled in a loop with large matrices

Authors:
  - Hugo Linsenmaier (https://github.com/hlinsen)

Approvers:
  - Ramakrishnap (https://github.com/rgsl888prabhu)
  - Rajesh Gandham (https://github.com/rg20)

URL: NVIDIA#1217
Merge the standalone libmps_parser C++ library and the cuopt-mps-parser Python wheel into the main cuopt build, eliminating the separate distribution while keeping all functionality. The parser sources now live under cpp/src/io/ and the public headers under cpp/include/cuopt/linear_programming/io/, matching the new namespace cuopt::linear_programming::io.

C++:
- Move cpp/libmps_parser/{src,include,tests}/ into the cuopt tree.
- Drop the separate libmps_parser.so target; parser sources compile into libcuopt.so via cpp/src/io/CMakeLists.txt.
- Hoist BZip2/ZLIB find_package + CUOPT_PARSER_WITH_{BZIP2,ZLIB} options to the top-level cpp build.
- Rename namespace cuopt::mps_parser -> cuopt::linear_programming::io across all sources, headers, tests, and benchmarks.
- Wire the gtest suite (mps_parser_test.cpp) into cpp/tests/linear_programming/CMakeLists.txt.

Python:
- Move python/cuopt/cuopt/linear_programming/cuopt_mps_parser/ into python/cuopt/cuopt/linear_programming/mps_parser/, importable as cuopt.linear_programming.mps_parser (and re-exported as ParseMps/toDict at the linear_programming package level).
- Fold data_model/ cython binding into the main cuopt wheel; relink data_model and solver cython modules to cuopt::cuopt.
- Update cython .pxd extern paths and namespace strings to cuopt/linear_programming/io/ and cuopt::linear_programming::io.
- Delete the standalone parser-wheel scaffolding (pyproject.toml, CMakeLists.txt, README, LICENSE, _version.py).
- Update test imports across test_parser.py, test_lp_solver.py, test_incumbent_callbacks.py, test_cpu_only_execution.py, test_pdlp_warmstart.py, plus cuopt_self_host_client.py, problem.py, and regression/benchmark_scripts/utils.py.

Build / packaging / CI:
- Drop the libmps_parser target and the FIND_MPS_PARSER_CPP flag from build.sh; help text and default action updated accordingly.
- Delete conda/recipes/mps-parser/ and the libmps-parser output (plus all pin_subpackage references) in conda/recipes/libcuopt/recipe.yaml.
- Drop cuopt-mps-parser =${version} from conda/recipes/cuopt/recipe.yaml.
- Remove the wheel-build-cuopt-mps-parser job, mps_parser_filter, and ci/build_wheel_cuopt_mps_parser.sh from CI; clean up cuopt_mps_parser wheelhouse downloads/installs from the remaining ci/*.sh scripts.
- Drop cuopt-mps-parser dependency entries from python/{cuopt,libcuopt, cuopt_self_hosted}/pyproject.toml and dependencies.yaml; remove the py_*_cuopt_mps_parser pyproject groups and depends_on_mps_parser.
- Update Dockerfile and ci/release/update-version.sh package lists.

MPS/QPS parsing functionality is now bundled directly into the `cuopt` Python package. The standalone `cuopt-mps-parser` package (and its `cuopt_mps_parser` and `data_model` import names) are no longer published.

  If you depend on the parser, you need to update both your environment and your imports.

  ### Environment

  **Conda users.** Remove `cuopt-mps-parser` from your environment files; `cuopt` now bundles the parser:

  ```yaml
  # conda environment file (before)
  dependencies:
    - cuopt=26.6
    - cuopt-mps-parser=26.6   # <- delete this line

  # (after)
  dependencies:
    - cuopt=26.6
  ```

  **pip users.** Remove `cuopt-mps-parser` from your `requirements.txt` / `pyproject.toml` dependencies. `cuopt` (and `libcuopt`, `cuopt-server`,
  `cuopt-sh-client`) no longer require it transitively:

  ```diff
   cuopt-cu13==26.6.*
  -cuopt-mps-parser==26.6.*
  ```

  Run `pip uninstall cuopt-mps-parser` in existing environments to clear the old install.

  ### Code

  The functions and classes are unchanged — only the import paths moved. Update as follows:

  | Before | After |
  | --- | --- |
  | `import cuopt_mps_parser` | `from cuopt.linear_programming import mps_parser` |
  | `cuopt_mps_parser.ParseMps(...)` | `mps_parser.ParseMps(...)` |
  | `cuopt_mps_parser.toDict(...)` | `from cuopt.linear_programming.mps_parser import toDict` then `toDict(...)` |
  | `cuopt_mps_parser.parser_wrapper.DataModel` | `mps_parser.parser_wrapper.DataModel` |
  | `from cuopt_mps_parser.utilities import InputValidationError` | `from cuopt.linear_programming.mps_parser.utilities import InputValidationError` |
  | `from data_model import DataModel` | `from cuopt.linear_programming.data_model import DataModel` |

  For convenience, `ParseMps` is also re-exported at the package level:

  ```python
  from cuopt.linear_programming import ParseMps
  ```

Authors:
  - Miles Lubin (https://github.com/mlubin)
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Trevor McKay (https://github.com/tmckayus)
  - Yuwen Chen (https://github.com/yuwenchen95)

URL: NVIDIA#1193
…VIDIA#1224)

Updates `skills/cuopt-developer` to capture the reviewer feedback from NVIDIA#1194:

- Expand the PR-description rule with an explicit "don't include" list (how-it-works walkthroughs, file tables, exhaustive test-plan checklists).
- Add a new "Editing CI scripts and workflows" section: prefer extending existing scripts, don't restate framework defaults, no fallback values for required inputs, hard-code GitHub URLs, validate early, split chained bash commands.

Goal is to keep future agent-authored PRs that touch `ci/` or `.github/workflows/` from generating the same review round-trips.

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Trevor McKay (https://github.com/tmckayus)
  - Miles Lubin (https://github.com/mlubin)

URL: NVIDIA#1224
## Summary
- Fix an out-of-bounds host read in `adapted_sol_t::priority_remove_diff_routes` by sorting route ids directly on `routes[id].length`.
- The old comparator indexed a compact `route_priority` vector by route id, which could corrupt the host heap and intermittently abort `ROUTING_TEST`.

Closes NVIDIA#1221. Closes NVIDIA#866. Closes NVIDIA#783.

## Validation
Running many times under ASan no longer triggers the SIGABRT, though it was very hard to reproduce the previous failure before this fix.

Authors:
  - Bradley Dice (https://github.com/bdice)

Approvers:
  - Hugo Linsenmaier (https://github.com/hlinsen)

URL: NVIDIA#1222
…job level (NVIDIA#1229)

## Summary

Closes **6 SonarQube findings** on `main` — 1 CRITICAL VULN and 5 MAJOR VULNs — without changing runtime behavior.

### `ci/test_self_hosted_service.sh` — suppress `shell:S4830`
The `curl -k` on the health probe is intentional: this same script generates a self-signed cert and starts the cuOpt server two lines earlier, so there is no real TLS endpoint to validate against (subsequent `cuopt_sh` calls *do* validate using the test CA in `$CLIENT_CERT`). Added a `# NOSONAR` marker with rationale.

### `.github/workflows/build_test_publish_images.yaml` — job-scoped permissions
Moved the workflow-level `permissions:` block to per-job blocks (rule `githubactions:S8264`/`S8233`). After reading the two reusable workflows (`build_images.yaml`, `test_images.yaml`), every job only does `actions/checkout` + DockerHub/NGC logins via username/password secrets — no OIDC, no GHCR pull, no artifact download, no PR API. So each job is reduced to `contents: read` only, dropping unused `actions: read`, `id-token: write`, `packages: read`, `pull-requests: read`.

Authors:
  - Ramakrishnap (https://github.com/rgsl888prabhu)

Approvers:
  - Bradley Dice (https://github.com/bdice)

URL: NVIDIA#1229
@chris-maes chris-maes requested a review from a team as a code owner May 18, 2026 21:06
@chris-maes chris-maes requested a review from bdice May 18, 2026 21:06
@chris-maes
Copy link
Copy Markdown
Contributor Author

For some reason I pulled in a ton of unwanted changes. Closing and moving to PR #1237

@chris-maes chris-maes closed this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.