Skip to content

Replace hardcoded Lanczos eigenvalue goldens with property-based checks - #3086

Open
says1117 wants to merge 11 commits into
NVIDIA:mainfrom
says1117:fix-2519-lanczos-hardcoded-tests
Open

Replace hardcoded Lanczos eigenvalue goldens with property-based checks#3086
says1117 wants to merge 11 commits into
NVIDIA:mainfrom
says1117:fix-2519-lanczos-hardcoded-tests

Conversation

@says1117

Copy link
Copy Markdown
Contributor

Fixes #2519.

The problem

The Lanczos gtests checked computed eigenvalues against hardcoded arrays (devArrMatch + CompareApprox(1e-5)). That's fragile: floating-point addition isn't associative, so cuSPARSE/cuBLAS can sum things in a different order depending on CUDA version and GPU architecture, and the last few digits of an eigenvalue shift. #3021 hit this on A100/ARM64 — 0.01367824 vs 0.01369178, a difference just outside the tolerance, even though the solve was correct.

The fix

Instead of comparing against a fixed answer, check that what came back is actually a valid eigenpair:

  • Residual: ||A*v - λv|| should be tiny. This is the same thing the solver's own tolerance config is defined to converge on, so it's not an arbitrary check - it's the solver's own success criterion.
  • Rayleigh quotient: λ ≈ (vᵀAv)/(vᵀv). Added this because the residual alone gets loose on matrices with a large norm, and can miss a small eigenvalue being slightly wrong. The Rayleigh quotient pins it down directly.
  • Unit norm: eigenvectors should have length 1.
  • Orthogonality: distinct eigenvectors shouldn't overlap. This one isn't in the old test at all - it catches a specific Lanczos failure mode where the solver returns the same eigenpair twice ("ghost duplicates"), which the residual check alone can't see.
  • Ordering: eigenvalues come back sorted ascending, which I confirmed against all 11 of the old golden arrays before deleting them.

rmat_lanczos_tests already had the matrix-vector-multiply helper needed for the residual check sitting there unused; I just started using it. lanczos_tests needed it added. Eigenvectors were already being computed by every test, just never checked - no solver changes needed.

Left the reproducibility check alone (it runs the solve twice with the same seed and diffs the results exactly - that's a determinism check, not a golden-value comparison, so it isn't affected by the cross-platform issue).

Picking the tolerance

I didn't want to just guess a number, so I instrumented the checks, ran all 11 test fixtures, and looked at what the residuals actually came out to:

fixture residual ÷ (‖A‖·ε) orthogonality ÷ ε |λ − Rayleigh| ÷ (‖A‖·ε)
double, n=100, ‖A‖=8.2 2.7 2.7 1.7
float, n=100, ‖A‖=8.2 0.7 2.5 0.9
double, n=100, ‖A‖=10.5 14.2 2.1 18.8
float, n=100, ‖A‖=10.5 8.6 0.4 4.9
float, n=4096, ‖A‖=298 4.8 18.9 2.0

Two things stood out. First, the residual scales with ‖A‖ · machine epsilon, not with the matrix size n - the small fixtures (n=100) and the big RMAT one (n=4096) land in the same range. That mattered a lot (see below). Second, orthogonality and unit-norm error are both just small multiples of epsilon.

So the tolerances ended up being:

```
matrix_tol = 500 × max(‖A‖, 1) × ε — residual, Rayleigh, ordering
unit_tol = 1000 × ε — unit norm, orthogonality
```

Every measured value sits at 0.04%–3.8% of its tolerance - comfortable margin for cross-platform noise, without being so loose the check stops meaning anything.

Making sure I didn't just weaken the test

I proved these checks can actually fail, by corrupting a solver's output on purpose and confirming the test catches it:

  • Scale every eigenvalue by 1% → caught by the residual and Rayleigh checks, on all 11 tests.
  • Zero out an eigenvector → caught immediately by the unit-norm check.
  • Copy eigenpair 0 into eigenpair 1's slot (still a perfectly valid, unit-norm, low-residual eigenpair — just a duplicate) → only the orthogonality check catches this. Everything else passes. This is exactly why that check earns its place.

Worth being honest about: my first attempt at this got it wrong. I'd included an n factor in the residual tolerance, and it turned out RmatLanczosTestF still passed even with every eigenvalue scaled by 1% - a real regression from the old test, which would've caught that instantly. Digging into the data above is what showed the residual doesn't actually scale with n, and once I fixed that (and added the Rayleigh check), the same corruption test failed cleanly across the board, including on the hardest case - the smallest eigenvalue in the RMAT set, where a 1% error is only 0.08 in absolute terms.

Verification

Built and ran on an RTX 2060 SUPER (sm_75), CUDA 13.3, WSL2:

```
$ cmake --build cpp/build -j2 --target SOLVERS_TEST
[2/2] Linking CXX executable gtests/SOLVERS_TEST

$ ./cpp/build/gtests/SOLVERS_TEST --gtest_filter='Lanczos'
[==========] 11 tests from 11 test suites ran. (162255 ms total)
[ PASSED ] 11 tests.
```

pre-commit passes clean.

For reviewers

Refs #2519, #3021.

Golden-value comparisons via devArrMatch/CompareApprox were flaky across
CUDA versions/architectures because FP reduction order isn't deterministic.
Replace with residual, orthogonality, unit-norm, and ordering checks that
verify the defining properties of an eigenpair instead of exact values.

Fixes NVIDIA#2519
@copy-pr-bot

copy-pr-bot Bot commented Jul 20, 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.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 66ac43c4-663c-48bc-91fc-ce69a87d125b

📥 Commits

Reviewing files that changed from the base of the PR and between a193d93 and b735eb7.

📒 Files selected for processing (1)
  • cpp/tests/sparse/solver/lanczos.cu
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/tests/sparse/solver/lanczos.cu

📝 Walkthrough

Summary by CodeRabbit

  • Tests
    • Strengthened Lanczos eigensolver checks for eigenvalue ordering, normalization, orthogonality, Rayleigh-quotient consistency, and residual accuracy.
    • Applied consistent validation to CSR and COO matrices using Frobenius-scaled tolerances.
    • Verified smallest and largest algebraic or magnitude-based eigenvalue selections against an independently computed spectrum.
    • Removed reliance on hardcoded expected eigenvalues while retaining reproducibility checks across test fixtures.

Walkthrough

Lanczos tests now validate eigenpair properties and requested eigenvalue subsets. They compute tolerances from matrix scale and remove hardcoded expected eigenvalue data from RMAT and fixed-matrix fixtures.

Changes

Lanczos validation

Layer / File(s) Summary
Eigenpair and spectrum validation helpers
cpp/tests/sparse/solver/lanczos.cu
Adds scaled tolerances, intrinsic eigenpair checks, dense spectrum computation, and selection checks for SA, SM, LA, and LM.
CSR and COO solver validation
cpp/tests/sparse/solver/lanczos.cu
Updates RMAT and fixed-matrix tests to validate eigenpairs and independently computed eigenvalue subsets.
Lanczos fixture and input cleanup
cpp/tests/sparse/solver/lanczos.cu
Removes expected eigenvalue members, setup state, allocations, obsolete wrappers, and hardcoded fixture eigenvalue tails.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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: replacing hardcoded Lanczos eigenvalue goldens with property-based checks.
Description check ✅ Passed The description directly explains the test changes, tolerance design, verification results, and rationale for removing hardcoded eigenvalue comparisons.
Linked Issues check ✅ Passed The changes remove hardcoded Lanczos results and add property-based checks that preserve and strengthen test coverage as required by issue #2519.
Out of Scope Changes check ✅ Passed The changes are limited to Lanczos test fixtures, helpers, validations, and tolerance handling, with no unrelated solver or production-code changes.
✨ 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 `@cpp/tests/sparse/solver/lanczos.cu`:
- Around line 79-87: Synchronize the RAFT stream before every host read of
results produced by host-scalar `raft::linalg::dot` in `compute_frobenius_norm`
and the affected Lanczos calculations involving `rayleigh`, `residual_sq`,
`v_norm_sq`, and `dot_ij`. Use the RAFT stream synchronization helper, and
replace the existing raw `cudaStreamSynchronize` with that helper.
🪄 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: 74ae28a5-be53-450a-8676-c9ef4fdfbe94

📥 Commits

Reviewing files that changed from the base of the PR and between d2bff5d and 3547c6a.

📒 Files selected for processing (1)
  • cpp/tests/sparse/solver/lanczos.cu

Comment thread cpp/tests/sparse/solver/lanczos.cu
@says1117

Copy link
Copy Markdown
Contributor Author

Test files aren't in Doxygen scope per cpp/REVIEW_GUIDELINES.md

@divyegala

Copy link
Copy Markdown
Contributor

/ok to test b9cd88a

@aamijar aamijar added non-breaking Non-breaking change bug Something isn't working improvement Improvement / enhancement to an existing function and removed bug Something isn't working labels Jul 21, 2026
@aamijar

aamijar commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Hi @says1117, it looks like the lanczos cpp tests aren't passing with this change.

says1117 added 2 commits July 22, 2026 21:48
…leigh quotient

CI showed LanczosTestD_SM failing on H100 and V100: the smallest-magnitude
eigenVECTOR residual is ill-conditioned and varies across CUDA versions and
GPU architectures, while the eigenVALUE stays correct on every platform
(Rayleigh quotient passes everywhere). Split the checks into eigenvalue-
accuracy (ordering, Rayleigh -- held tight for all modes) and eigenvector-
accuracy (residual, norm, orthogonality -- relaxed for SM only). Non-SM and
all eigenvalue tolerances are unchanged, so no previously-passing test can
regress. See NVIDIA#2705, NVIDIA#2758.
…7/raft into fix-2519-lanczos-hardcoded-tests
@cjnolet

cjnolet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

/ok to test

@copy-pr-bot

copy-pr-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

/ok to test

@cjnolet, there was an error processing your request: E1

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/1/

@says1117

Copy link
Copy Markdown
Contributor Author

CI surfaced that LanczosTestD_SM fails on H100/V100 while passing on A100/L4/RTX.

Root cause: the smallest-magnitude eigenvector is ill-conditioned and its residual varies by orders of magnitude across CUDA versions/architectures, but the eigenvalue is correct on every platform (the Rayleigh quotient vᵀAv/vᵀv agrees to full tolerance everywhere), matching the SM concerns in #2705/#2758.

Rather than loosen everything, I split the checks: eigenvalue accuracy (Rayleigh + ordering) stays tight for all modes, and only the SM eigenvector bounds (residual/norm/orthogonality) are relaxed. Non-SM and all eigenvalue tolerances are unchanged. I confirmed a 1% eigenvalue perturbation on SM is still caught by the Rayleigh check, so the SM test isn't vacuous. Flagging in case the underlying SM eigenvector conditioning warrants separate solver investigation.

@says1117

Copy link
Copy Markdown
Contributor Author

@cjnolet thanks for trying to kick this off! Looks like that /ok to test didn't include the commit SHA. Here's the current one if it helps: 2435316

@aamijar

aamijar commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

/ok to test 10d8ab5

@says1117

Copy link
Copy Markdown
Contributor Author

@divyegala Just following up. This has had a passing CI run and CodeRabbit's automated review since Jul 23 with no outstanding actionable comments, but it's still waiting on a human review or approval.

@divyegala

Copy link
Copy Markdown
Contributor

Hi @says1117 thanks for following up! @aamijar could you please review this PR?

@aamijar aamijar 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 contribution @says1117!
My main concern is that we aren't validating the which parameter. How do we know if the eigenvalues returned are SA, SM, LA, LM ?

Comment thread cpp/tests/sparse/solver/lanczos.cu Outdated
*/
template <typename IndexType, typename ValueType>
void expect_valid_eigenpairs(raft::resources const& handle,
raft::spectral::matrix::sparse_matrix_t<IndexType, ValueType> const& A,

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.

Another smaller detail: we would prefer to use the newer device_csr_matrix_view type here instead

…e_csr_matrix_view

Address review: expect_valid_eigenpairs only confirmed returned pairs were
*some* genuine eigenpairs of A, not that they were the correct SA/SM/LA/LM
selection. Add compute_full_spectrum() (dense CSR->eigDC, independent of
Lanczos) and expect_correct_selection() to check against it. Also switch
the sparse-matrix parameter to device_csr_matrix_view, matching the type
already used at the solve call sites, instead of the legacy sparse_matrix_t
wrapper.

@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

🧹 Nitpick comments (2)
cpp/tests/sparse/solver/lanczos.cu (2)

532-533: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Note the dense reference cost for the RMAT fixture.

compute_full_spectrum densifies the RMAT matrix. With r_scale = c_scale = 12 this allocates two 4096x4096 matrices (about 64 MiB each) and runs a dense eigensolve of O(n^3). This is acceptable today. If the RMAT scales increase later, memory grows as O(n^2) and runtime as O(n^3). Consider bounding the dense selection check to a smaller RMAT fixture, or documenting the scale limit next to rmat_inputsf.

🤖 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/sparse/solver/lanczos.cu` around lines 532 - 533, Document the
supported RMAT scale limit next to rmat_inputsf, including that
compute_full_spectrum uses dense O(n^2) memory and O(n^3) runtime.
Alternatively, restrict the dense selection check to a smaller RMAT fixture
while preserving the existing validation behavior.

348-351: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use eig_dc with mdspan views.

This removes raw pointers and the explicit stream argument. The mdspan type also makes the column-major layout explicit.

🤖 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/sparse/solver/lanczos.cu` around lines 348 - 351, Update the
reference eigensolver call in the Lanczos test to use eig_dc with mdspan views
for dense, ref_vectors, and ref_values instead of raw data handles. Preserve the
column-major layout in the mdspan types and remove the explicit stream argument.

Sources: Coding guidelines, Path instructions

🤖 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/sparse/solver/lanczos.cu`:
- Around line 380-386: Add an assertion before the subset extraction in the
Lanczos test to require n_components to be smaller than the full spectrum/matrix
dimension, preventing iterator advancement beyond the available range. Apply
this guard consistently across all four branches that use n_components for
iterator arithmetic.

---

Nitpick comments:
In `@cpp/tests/sparse/solver/lanczos.cu`:
- Around line 532-533: Document the supported RMAT scale limit next to
rmat_inputsf, including that compute_full_spectrum uses dense O(n^2) memory and
O(n^3) runtime. Alternatively, restrict the dense selection check to a smaller
RMAT fixture while preserving the existing validation behavior.
- Around line 348-351: Update the reference eigensolver call in the Lanczos test
to use eig_dc with mdspan views for dense, ref_vectors, and ref_values instead
of raw data handles. Preserve the column-major layout in the mdspan types and
remove the explicit stream argument.
🪄 Autofix

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: e77693a0-55e1-4b61-a610-e8b8c98d84dd

📥 Commits

Reviewing files that changed from the base of the PR and between 2435316 and a193d93.

📒 Files selected for processing (1)
  • cpp/tests/sparse/solver/lanczos.cu

Comment thread cpp/tests/sparse/solver/lanczos.cu
@says1117

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @aamijar!

which validation. Added compute_full_spectrum(), which densifies the matrix and runs raft::linalg::eig_dc (cuSOLVER's dense symmetric eigensolver, independent of Lanczos and computed fresh rather than hardcoded), plus expect_correct_selection() to check the returned eigenvalues against the correct SA/SM/LA/LM subset of that reference. Verified it actually discriminates by feeding it a deliberately wrong criterion (SA output checked against LA) and confirming it fails.

device_csr_matrix_view. Switched as suggested, which also let me drop the separate sparse_matrix_t construction at each call site.

CodeRabbit nitpicks. Documented the dense-eigensolve cost at RMAT's scale (n=4096) and used the eig_dc mdspan wrapper.

Bounds guard. Added an ASSERT_LE so n_components exceeding the matrix dimension fails cleanly instead of running off the end of the iterators.

@says1117
says1117 requested a review from aamijar August 12, 2026 06:46
@aamijar

aamijar commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

/ok to test 45e4085

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvement / enhancement to an existing function non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Make new lanczos gtests more robust to varying cuda runtime versions

4 participants