Skip to content

Fix the bug detecting nonconvex quadratic constraints in the general …#1438

Open
yuwenchen95 wants to merge 1 commit into
NVIDIA:mainfrom
yuwenchen95:fix-qcqp-nonconvex-reject
Open

Fix the bug detecting nonconvex quadratic constraints in the general …#1438
yuwenchen95 wants to merge 1 commit into
NVIDIA:mainfrom
yuwenchen95:fix-qcqp-nonconvex-reject

Conversation

@yuwenchen95

Copy link
Copy Markdown
Contributor

Description

Cross-only indefinite quadratic constraints (e.g. 2·x₀·x₁ ≤ 0.5 with H = [[0,2],[2,0]]) previously passed convexity checks on the general SOC conversion path. Diagonal LDLT returned rank = 0 without INDEFINITE_MATRIX_RETURN, and a degenerate r = 0 SOC lift silently dropped the quadratic term, allowing wrong solutions to be reported as Optimal (e.g. obj=2 at (1,1) instead of rejecting the problem).

Fixes:

  • right_looking_ldlt: return INDEFINITE_MATRIX_RETURN when factorization stalls at rank = 0 on a nonzero matrix.
  • translate_soc.hpp (general path): cuopt_expects(rank >= 1, …) before building the SOC lift.

Adds LDLT and SOC-conversion unit tests for the cross-only indefinite case.

Issue

closes #1434

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

Test plan

  • ./build.sh libcuopt with conda env active, then run built/installed cuopt_cli against $CONDA_PREFIX/lib/libcuopt.so
  • C++ GTest:
    • ./cpp/build/tests/dual_simplex/DUAL_SIMPLEX_TEST --gtest_filter='right_looking_ldlt.indefinite_cross_only_2x2'
    • ./cpp/build/tests/socp/SOCP_TEST --gtest_filter='general_quadratic.rejects_cross_only_indefinite'
    • ./cpp/build/tests/socp/SOCP_TEST --gtest_filter='general_quadratic.rejects_non_convex'
  • Manual: datasets/qcqp/issue_1434_nonconvex.lpValidationError (not Optimal obj=2)

@yuwenchen95 yuwenchen95 requested a review from a team as a code owner June 15, 2026 18:27
@yuwenchen95 yuwenchen95 added bug Something isn't working non-breaking Introduces a non-breaking change labels Jun 15, 2026
…path

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
@yuwenchen95 yuwenchen95 force-pushed the fix-qcqp-nonconvex-reject branch from cedb27f to 07f7b3f Compare June 15, 2026 18:29
@yuwenchen95 yuwenchen95 assigned yuwenchen95 and unassigned mlubin, rg20 and chris-maes Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 15, 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: 7209e895-9044-43b8-b5f7-4df5d97c389c

📥 Commits

Reviewing files that changed from the base of the PR and between cedb27f and 07f7b3f.

📒 Files selected for processing (5)
  • cpp/src/barrier/translate_soc.hpp
  • cpp/src/dual_simplex/right_looking_lu.cpp
  • cpp/src/dual_simplex/right_looking_lu.hpp
  • cpp/tests/dual_simplex/unit_tests/right_looking_ldlt.cpp
  • cpp/tests/socp/general_quadratic_test.cu
✅ Files skipped from review due to trivial changes (1)
  • cpp/src/dual_simplex/right_looking_lu.hpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • cpp/src/barrier/translate_soc.hpp
  • cpp/tests/dual_simplex/unit_tests/right_looking_ldlt.cpp
  • cpp/tests/socp/general_quadratic_test.cu
  • cpp/src/dual_simplex/right_looking_lu.cpp

📝 Walkthrough

Walkthrough

Fixes a silent constraint-drop bug: right_looking_ldlt now returns INDEFINITE_MATRIX_RETURN when it finds no acceptable diagonal pivot on a nonzero matrix (cross-only indefinite case). convert_quadratic_constraints_to_second_order_cones adds a rank >= 1 guard that rejects these degenerate factorizations. Two new unit tests validate the fix end-to-end.

Changes

Cross-only indefinite QCQP detection and rejection

Layer / File(s) Summary
LDLT factorization: detect cross-only indefinite case
cpp/src/dual_simplex/right_looking_lu.hpp, cpp/src/dual_simplex/right_looking_lu.cpp
input_nnz is captured at entry. When symmetric_markowitz_search returns no pivot (pivot_p == -1) and no pivots have been chosen yet but the input was nonzero, right_looking_ldlt now returns INDEFINITE_MATRIX_RETURN instead of silently breaking. The INDEFINITE_MATRIX_RETURN doc comment is updated to cover both negative-pivot and no-acceptable-pivot scenarios. Unused #include <cstdio> is removed.
SOC translator: rank >= 1 guard after LDLT
cpp/src/barrier/translate_soc.hpp
Adds cuopt_expects(rank >= 1, ...) in convert_quadratic_constraints_to_second_order_cones after LDLT factorization, replacing the prior comment-only assumption. The error message reports the observed LDLT rank.
Unit tests for LDLT and SOC rejection
cpp/tests/dual_simplex/unit_tests/right_looking_ldlt.cpp, cpp/tests/socp/general_quadratic_test.cu
right_looking_ldlt.indefinite_cross_only_2x2 asserts INDEFINITE_MATRIX_RETURN for [[0,2],[2,0]]. general_quadratic.rejects_cross_only_indefinite asserts that convert_quadratic_constraints_to_second_order_cones throws cuopt::logic_error for the same Hessian.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • hlinsen
  • rg20
  • nguidotti
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive Title describes the main bug fix (detecting nonconvex quadratic constraints) but is truncated and incomplete. Complete the title to clearly describe the full scope of the fix, e.g., 'Fix detection of nonconvex cross-only indefinite quadratic constraints in general SOC path'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description thoroughly explains the bug, root cause, fixes implemented, and test coverage related to the changeset.
Linked Issues check ✅ Passed All code changes directly address issue #1434: right_looking_ldlt now returns INDEFINITE_MATRIX_RETURN for rank=0 cases, translate_soc.hpp validates rank >= 1, and unit tests cover the cross-only indefinite scenario.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the nonconvex quadratic constraint detection bug; no unrelated refactoring, formatting, or feature additions are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

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

@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/src/dual_simplex/right_looking_lu.cpp`:
- Around line 1686-1687: Add a new method `has_remaining_numerically_nonzero` to
the `symmetric_trailing_matrix_t` class that checks whether the trailing matrix
has any numerically nonzero elements by iterating through diagonal elements and
off-diagonal elements, comparing them against a numeric tolerance parameter.
Then, refine the indefinite return condition around line 1726 (in the 1723-1727
range) so that it returns indefinite not only when `pivots == 0`, but also when
the trailing matrix is still numerically nonzero after partial elimination,
ensuring mixed cases with one early pivot followed by remaining cross-only
indefinite blocks are properly handled.
🪄 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: 8a87302e-97ad-4c39-a947-ab3d6cfcd39e

📥 Commits

Reviewing files that changed from the base of the PR and between 03fe3fc and cedb27f.

📒 Files selected for processing (5)
  • cpp/src/barrier/translate_soc.hpp
  • cpp/src/dual_simplex/right_looking_lu.cpp
  • cpp/src/dual_simplex/right_looking_lu.hpp
  • cpp/tests/dual_simplex/unit_tests/right_looking_ldlt.cpp
  • cpp/tests/socp/general_quadratic_test.cu

Comment on lines +1686 to +1687
const i_t n = A.n;
const i_t input_nnz = A.nnz();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle no-pivot stalls after partial elimination as indefinite when the trailing matrix is still nonzero.

At Line 1726, the indefinite return is gated by pivots == 0, so mixed cases can still slip through (e.g., one valid early pivot, then a remaining cross-only indefinite block). That returns a positive rank and can still let non-convex QCQP pass conversion.

🛠️ Suggested fix
-  const i_t n         = A.n;
-  const i_t input_nnz = A.nnz();
+  const i_t n = A.n;
...
-    if (pivot_p == -1) {
-      // No acceptable diagonal pivot. Zero matrix is rank 0; nonzero cross-only indefinite
-      // matrices (e.g. [[0, a]; [a, 0]]) also stall here and must not be treated as PSD.
-      if (pivots == 0 && input_nnz > 0) { return INDEFINITE_MATRIX_RETURN; }
-      break;
-    }
+    if (pivot_p == -1) {
+      // If any significant entry remains in the trailing matrix, matrix is not PSD.
+      if (trailing_matrix.has_remaining_numerically_nonzero(pivot_tol)) {
+        return INDEFINITE_MATRIX_RETURN;
+      }
+      break;  // true rank-deficient zero remainder
+    }
// Add to symmetric_trailing_matrix_t
bool has_remaining_numerically_nonzero(f_t nz_tol) const
{
  for (i_t j = 0; j < n_; ++j) {
    if (std::abs(diag_[j]) >= nz_tol) { return true; }
    for (i_t p = col_start_[j]; p < col_end_[j]; ++p) {
      if (std::abs(c_x_[p]) >= nz_tol) { return true; }
    }
  }
  return false;
}

As per coding guidelines, solver reviews should catch degenerate/numerical transformation-path correctness gaps.

Also applies to: 1723-1727

🤖 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/src/dual_simplex/right_looking_lu.cpp` around lines 1686 - 1687, Add a
new method `has_remaining_numerically_nonzero` to the
`symmetric_trailing_matrix_t` class that checks whether the trailing matrix has
any numerically nonzero elements by iterating through diagonal elements and
off-diagonal elements, comparing them against a numeric tolerance parameter.
Then, refine the indefinite return condition around line 1726 (in the 1723-1727
range) so that it returns indefinite not only when `pivots == 0`, but also when
the trailing matrix is still numerically nonzero after partial elimination,
ensuring mixed cases with one early pivot followed by remaining cross-only
indefinite blocks are properly handled.

Source: Coding guidelines


// Test: cross-only indefinite Q (issue #1434). H = [[0, 2]; [2, 0]] has zero diagonals so LDLT
// returns rank 0 without a negative pivot; must still be rejected as non-convex.
TEST(general_quadratic, rejects_cross_only_indefinite)

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.

Instead of testing an internal converter, what about the end-to-end behavior on a non-convex instance? What status does the solver return?

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'm following the style of previous rejects_non_convex test, just to check if we can capture the error correctly.

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.

Here is the full info after the change:

(cuopt_dev)~:/cuopt_workspace/cuopt$ ./cpp/build/cuopt_cli ./datasets/qcqp/issue_1434_nonconvex.lp 
Setting CUDA_MODULE_LOADING to EAGER
Reading file issue_1434_nonconvex.lp
Read file issue_1434_nonconvex.lp in 0.01 seconds
cuOpt version: 26.8.0, git hash: 07f7b3f7, host arch: x86_64, device archs: 90a-real
CPU: AMD EPYC 7413 24-Core Processor, threads (physical/logical): 24/48, RAM: 211.39 GiB
CUDA 12.9, device: NVIDIA H200 (ID 0), VRAM: 139.80 GiB
CUDA device UUID: 9f8075aa-e02a-0953-1955-32f638fb2993

Problem has 1 quadratic constraints. Converting to second-order cones and solving with barrier.
Error in solve_qcqp: {"CUOPT_ERROR_TYPE": "ValidationError", "msg": "Quadratic constraint 'q0' is non-convex (Q matrix is indefinite)"}

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.

[BUG] incorrect "optimal" solution on nonconvex qcqp

4 participants