Skip to content

Fix Ruiz equilibration skip heuristic to also check column imbalance#1601

Open
mlubin wants to merge 6 commits into
NVIDIA:mainfrom
mlubin:fix-ruiz-skip-column-imbalance
Open

Fix Ruiz equilibration skip heuristic to also check column imbalance#1601
mlubin wants to merge 6 commits into
NVIDIA:mainfrom
mlubin:fix-ruiz-skip-column-imbalance

Conversation

@mlubin

@mlubin mlubin commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The skip heuristic in scaling() only measured row-norm imbalance, so a QP/SOCP with balanced rows but severely imbalanced columns (e.g. QPLIB_8515, where free variables appear only with ~1e-8 coefficients against O(1) rows) received no scaling at all. The resulting augmented KKT system was so ill-conditioned that barrier terminated at a suboptimal point while reporting convergence (320.13 vs the true optimum 320.00 on QPLIB_8515, non-deterministically across runs). With the column check, Ruiz runs and the instance solves to 319.999989 in 14 iterations instead of 338.

Adds QPLIB_8515 as a downloaded test dataset (new
download_qplib_test_dataset.sh, wired into ci/test_cpp.sh and CONTRIBUTING.md) and a full-solve regression test that asserts the optimal objective matches the QPLIB reference value.

A summary of impacts on public benchmark instances that are affected:

Improvements

Instance Source Iterations Solve time (s) Rel. obj. error Notes
QPLIB_8515 QPLIB 177 → 14 2.62 → 0.36 3.2e-04 → 9.4e-10 7.3× faster; now matches reference objective
PRIMALC1 Maros–Mészáros 23 → 16 0.13 → 0.13 0 → 4.7e-08
PRIMALC2 Maros–Mészáros 25 → 16 0.13 → 0.12 0 → 2.8e-08
PRIMALC5 Maros–Mészáros 21 → 15 0.13 → 0.12 0 → 1.9e-08
PRIMALC8 Maros–Mészáros 26 → 15 0.14 → 0.12 5.5e-09 → 5.5e-09
QPCBLEND Maros–Mészáros 17 → 17 0.13 → 0.14 5.6e-08 → 7.4e-09 accuracy only; time unchanged

Neutral

Instance Source Iterations Solve time (s) Rel. obj. error Notes
QPLIB_8938 QPLIB 18 → 17 0.43 → 0.40 2.1e-07 → 3.9e-07
DTOC3 Maros–Mészáros 12 → 12 0.83 → 0.83 0 → 0
STADAT2 Maros–Mészáros 20 → 17 0.86 → 0.81 0 → 3.6e-07
STADAT3 Maros–Mészáros 20 → 18 0.86 → 0.85 0 → 2.8e-07
dsNRL CBLIB 41 → 41 67.8 → 69.1 2.4e-07 → 2.4e-07
wbNRL CBLIB 28 → 29 25.9 → 28.4 5.3e-06 → 4.8e-06
firL1 CBLIB 24 → 24 28.2 → 30.0 3.5e-06 → 1.1e-05 both errors well inside 1e-4 tolerance
firLinf CBLIB 23 → 24 84.7 → 91.3 1.3e-06 → 4.0e-07 +8% time, better accuracy
firL1Linfalph CBLIB 36 → 37 71.4 → 76.4 1.3e-07 → 7.3e-08 +7% time, better accuracy

Regressions

Instance Source Iterations Solve time (s) Rel. obj. error Notes
db-plate-yield-line CBLIB 42 → 46 6.4 → 8.7 9.9e-07 → 1.4e-06 reproducible: main stable at 42 it / ~6 s; branch 47–56 it / 8.5–9.5 s across reruns
STADAT1 Maros–Mészáros 64 → 381 0.93 → 1.46 0 → 0 mixed: main flipped optimal/suboptimal across reruns; branch always optimal (193–381 it)
HUES-MOD Maros–Mészáros 10 → 26 0.12 → 0.14 7.0e-05 → 7.0e-05 iteration count only; sub-second solve
HUESTIS Maros–Mészáros 15 → 32 0.12 → 0.15 7.0e-05 → 7.0e-05 iteration count only; sub-second solve
QPLIB_9008 QPLIB 54.1 → 169.0 both variants fail numerically ("search direction computation failed"); branch takes 3.1× longer to fail

Adds a --qcqp_hyper_ruiz_equilibration flag to override the behavior.

@copy-pr-bot

copy-pr-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

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.

@mlubin
mlubin force-pushed the fix-ruiz-skip-column-imbalance branch 3 times, most recently from b38e814 to 8ba9578 Compare July 21, 2026 21:39
The skip heuristic in scaling() only measured row-norm imbalance, so a
QP/SOCP with balanced rows but severely imbalanced columns (e.g.
QPLIB_8515, where free variables appear only with ~1e-8 coefficients
against O(1) rows) received no scaling at all. The resulting augmented
KKT system was so ill-conditioned that barrier terminated at a
suboptimal point while reporting convergence (320.13 vs the true
optimum 320.00 on QPLIB_8515, non-deterministically across runs). With
the column check, Ruiz runs and the instance solves to 319.999989 in 14
iterations instead of 338.

Adds QPLIB_8515 as a downloaded test dataset (new
download_qplib_test_dataset.sh, wired into ci/test_cpp.sh and
CONTRIBUTING.md) and a full-solve regression test that asserts the
optimal objective matches the QPLIB reference value.

Signed-off-by: Miles Lubin <mlubin@nvidia.com>
@mlubin
mlubin force-pushed the fix-ruiz-skip-column-imbalance branch from 8ba9578 to ed675a1 Compare July 21, 2026 21:41
@mlubin mlubin added bug Something isn't working non-breaking Introduces a non-breaking change labels Jul 21, 2026
@mlubin

mlubin commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test ed675a1

@github-actions

Copy link
Copy Markdown

CI Test Summary

✅ All 0 test job(s) passed.

# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

INSTANCES=(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we only downloading this specific example?

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.

Yes, it's the first instance from qplib that we're using in tests.

Comment thread cpp/src/dual_simplex/scaling.cpp Outdated
}
f_t col_norm_ratio = (min_col_norm > 0) ? max_col_norm / min_col_norm : 1.0;

if (row_norm_ratio < 100.0 && col_norm_ratio < 100.0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We might want to add an option --row-scaling that by default is -1 and we use the heuristic below, but if the user sets --row-scaling=0 they can turn it off, and if they set --row-scaling=1 they can force it on.

The skip heuristic now checks both row-norm and column-norm imbalance,
but the comment still described only row-norm imbalance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Miles Lubin <mlubin@nvidia.com>
@mlubin
mlubin marked this pull request as ready for review July 22, 2026 21:37
@mlubin
mlubin requested review from a team as code owners July 22, 2026 21:37
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 28266a48-87f7-419f-b7f7-38a8584b58b8

📥 Commits

Reviewing files that changed from the base of the PR and between 92e20ef and a6bcbfa.

📒 Files selected for processing (1)
  • cpp/src/dual_simplex/scaling.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/src/dual_simplex/scaling.cpp

📝 Walkthrough

Walkthrough

Changes

The QPLIB downloader now supports S3 with HTTP fallback and is wired into CI and contributor instructions. QCQP Ruiz equilibration is configurable, and barrier scaling uses both row and column imbalance ratios with QPLIB regression tests.

QPLIB scaling regression

Layer / File(s) Summary
QPLIB dataset acquisition and wiring
datasets/quadratic_programming/download_qplib_test_dataset.sh, ci/test_cpp.sh, CONTRIBUTING.md, .gitignore
Adds S3-first QPLIB downloads with HTTP fallback, CI and documentation commands, and an ignore rule for downloaded instances.
QCQP Ruiz configuration plumbing
cpp/include/cuopt/mathematical_optimization/constants.h, cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp, cpp/src/dual_simplex/simplex_solver_settings.hpp, cpp/src/math_optimization/solver_settings.cu, cpp/src/pdlp/solve.cu
Declares, validates, initializes, and forwards the QCQP Ruiz equilibration setting into barrier solver settings.
Column-aware Ruiz scaling regression
cpp/src/dual_simplex/scaling.cpp, cpp/tests/dual_simplex/unit_tests/solve_barrier.cu
Uses row and column norm ratios for automatic Ruiz-scaling decisions and adds QPLIB_8515 barrier tests for enabled and forced-off behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • NVIDIA/cuopt#1290: Both changes modify barrier-specific configuration in run_barrier.

Suggested labels: barrier

Suggested reviewers: msarahan, chris-maes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: expanding the Ruiz equilibration skip heuristic to consider column imbalance too.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the heuristic change, dataset addition, and regression test.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@datasets/quadratic_programming/download_qplib_test_dataset.sh`:
- Line 1: Update the dataset download script to enable strict shell error
handling and ensure a failed wget for QPLIB_8515.lp exits with a nonzero status
instead of being masked by continue.
🪄 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: Enterprise

Run ID: 4a58299f-8732-465d-aa64-49ef9aeee586

📥 Commits

Reviewing files that changed from the base of the PR and between 077e514 and d43e238.

📒 Files selected for processing (6)
  • .gitignore
  • CONTRIBUTING.md
  • ci/test_cpp.sh
  • cpp/src/dual_simplex/scaling.cpp
  • cpp/tests/dual_simplex/unit_tests/solve_barrier.cu
  • datasets/quadratic_programming/download_qplib_test_dataset.sh

Comment thread datasets/quadratic_programming/download_qplib_test_dataset.sh

@ramakrishnap-nv ramakrishnap-nv left a comment

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.

A minor suggestion

Comment thread .gitignore
!datasets/quadratic_programming
!datasets/quadratic_programming/**
# downloaded QPLIB instances (see download_qplib_test_dataset.sh)
datasets/quadratic_programming/qplib/

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.

Suggested change
datasets/quadratic_programming/qplib/
!datasets/quadratic_programming/qplib/

Should this be

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.

I think this is correct because we want to ignore the downloaded instances, while not ignoring the checked-in instances.

@rg20 rg20 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the improvements!

@mlubin

mlubin commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test d43e238

mlubin added 3 commits July 23, 2026 21:29
Signed-off-by: Miles Lubin <mlubin@nvidia.com>
Signed-off-by: Miles Lubin <mlubin@nvidia.com>
Drop the redundant force-on solve from the toggle test (it re-treads the equilibrated path); force-off now asserts a >50-iteration blow-up, and the default-path test bounds the equilibrated solve to <30 iterations. Remove the redundant Python registry test (already covered by test_solver_settings) and trim the redundant "hidden from --help" note from the QCQP comments.

Signed-off-by: Miles Lubin <mlubin@nvidia.com>
@mlubin

mlubin commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 92e20ef

The tri-state switch in scaling() was hand-aligned in a way clang-format rejects; reformat to satisfy ci/check_style.sh.

Signed-off-by: Miles Lubin <mlubin@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/tests/dual_simplex/unit_tests/solve_barrier.cu`:
- Around line 266-269: Update the test around solve_lp and the existing solution
object to first assert optimal termination and verify the objective matches the
319.9999 reference value, then retain the iteration-count assertion. Ensure the
numerical correctness checks occur before validating that more than 50 steps
were taken.
🪄 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: Enterprise

Run ID: 6e24c9fd-4b16-4a8e-86ba-67ccfdf64b5d

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea14b8 and 92e20ef.

📒 Files selected for processing (3)
  • cpp/include/cuopt/mathematical_optimization/constants.h
  • cpp/src/math_optimization/solver_settings.cu
  • cpp/tests/dual_simplex/unit_tests/solve_barrier.cu
🚧 Files skipped from review as they are similar to previous changes (2)
  • cpp/include/cuopt/mathematical_optimization/constants.h
  • cpp/src/math_optimization/solver_settings.cu

Comment on lines +266 to +269
auto solution = solve_lp(&handle, mps_data, settings);

const int iters = solution.get_additional_termination_information().number_of_steps_taken;
EXPECT_GT(iters, 50);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a successful solve before asserting the iteration blow-up.

This test currently passes for any result taking more than 50 steps, including a non-optimal or iteration-limited termination. Assert optimal termination and the 319.9999 reference objective before checking the iteration count.

As per path instructions, regression tests must validate numerical correctness, not only iteration behavior.

Proposed fix
   auto solution = solve_lp(&handle, mps_data, settings);
 
+  ASSERT_EQ(solution.get_termination_status(), pdlp_termination_status_t::Optimal);
+  EXPECT_NEAR(solution.get_objective_value(), 319.9999, 1e-2);
   const int iters = solution.get_additional_termination_information().number_of_steps_taken;
   EXPECT_GT(iters, 50);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto solution = solve_lp(&handle, mps_data, settings);
const int iters = solution.get_additional_termination_information().number_of_steps_taken;
EXPECT_GT(iters, 50);
auto solution = solve_lp(&handle, mps_data, settings);
ASSERT_EQ(solution.get_termination_status(), pdlp_termination_status_t::Optimal);
EXPECT_NEAR(solution.get_objective_value(), 319.9999, 1e-2);
const int iters = solution.get_additional_termination_information().number_of_steps_taken;
EXPECT_GT(iters, 50);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/dual_simplex/unit_tests/solve_barrier.cu` around lines 266 - 269,
Update the test around solve_lp and the existing solution object to first assert
optimal termination and verify the objective matches the 319.9999 reference
value, then retain the iteration-count assertion. Ensure the numerical
correctness checks occur before validating that more than 50 steps were taken.

Source: Path instructions

@mlubin

mlubin commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test a6bcbfa

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.

5 participants