Skip to content

Add sparse Lanczos SVD solver#3034

Open
Intron7 wants to merge 4 commits into
NVIDIA:mainfrom
Intron7:feat/add-lanczos-svds
Open

Add sparse Lanczos SVD solver#3034
Intron7 wants to merge 4 commits into
NVIDIA:mainfrom
Intron7:feat/add-lanczos-svds

Conversation

@Intron7

@Intron7 Intron7 commented May 21, 2026

Copy link
Copy Markdown
Contributor

As discussed previously with @cjnolet I'm also adding my Lanczos SVD solver for sparse CSR matrices.

This is the more precise sparse SVD path next to the existing randomized solver. The solver repeatedly applies A @ v and A.T @ u to build Krylov bases, computes the SVD of the small bidiagonal problem, uses the resulting Ritz vectors to identify converged singular triplets, locks those vectors, and restarts on the remaining unconverged part. It also uses full reorthogonalization and a final A @ V refinement step to improve singular values and left singular vectors.

Compared with randomized SVD, this is aimed at quality: clustered spectra, slow singular-value decay, near-rank-deficient inputs, and PCA workloads where ARPACK-like accuracy matters.

Ran the #2999 -style row sweep on the same singlecell dataset, k=50, n_oversamples=10, n_power_iters=2, best-of-3 GPU timings. GPU: RTX PRO 6000 Blackwell.



  ┌──────┬───────┬─────────────────┬──────────────┬─────────────────────────┬─────────────────────┬──────────────────┐
  │ rows │   nnz │ raft randomized │ raft Lanczos │          Lanczos / rand │ randomized residual │ Lanczos residual │
  ├──────┼───────┼─────────────────┼──────────────┼─────────────────────────┼─────────────────────┼──────────────────┤
  │  50k │  101M │          0.180s │       0.252s │            1.40x slower │            3.11e-02 │         1.55e-07 │
  │ 200k │  400M │          0.698s │       1.328s │            1.90x slower │            3.12e-02 │         1.57e-07 │
  │ 500k │ 1.02B │          1.776s │       1.763s │          basically tied │            3.06e-02 │         1.57e-07 │
  │ 982k │ 2.01B │          3.536s │       3.423s │ Lanczos slightly faster │            3.09e-02 │         1.56e-07 │
  └──────┴───────┴─────────────────┴──────────────┴─────────────────────────┴─────────────────────┴──────────────────┘

Using the 2999 CPU baselines for context:

  ┌──────┬─────────────┬──────────────┬─────────────────────────────┬──────────────────────────┐
  │ rows │ sklearn CPU │ scipy ARPACK │ randomized speedup vs scipy │ Lanczos speedup vs scipy │
  ├──────┼─────────────┼──────────────┼─────────────────────────────┼──────────────────────────┤
  │  50k │       7.38s │       18.69s │                        104x │                      74x │
  │ 200k │      25.61s │       62.49s │                         90x │                      47x │
  │ 500k │      64.97s │      153.40s │                         86x │                      87x │
  │ 982k │     126.16s │      307.04s │                         87x │                      90x │
  └──────┴─────────────┴──────────────┴─────────────────────────────┴──────────────────────────┘

@copy-pr-bot

copy-pr-bot Bot commented May 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.

@aamijar aamijar added non-breaking Non-breaking change feature request New feature or request labels May 26, 2026
@aamijar aamijar moved this to In Progress in Unstructured Data Processing May 26, 2026
@aamijar

aamijar commented May 26, 2026

Copy link
Copy Markdown
Contributor

Hi @Intron7, I haven't looked at this PR closely yet, but one thing to think about is that we should try and reuse parts of the existing lanczos eigensolver as much as possible. They should have some things in common right?

@aamijar

aamijar commented May 27, 2026

Copy link
Copy Markdown
Contributor

/ok to test eabeaa4

@Intron7

Intron7 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@aamijar the algorithm is different even is the name is similar. The two paths share the Lanczos name but the kernels are different algorithms: the eigensolver builds a symmetric tridiagonal via a one-vector recurrence and ritz-solves with syevd; the SVD builds a bidiagonal via Golub-Kahan with two coupled bases (A @ v and Aᵀ @ u) and ritz-solves with gesvdj. Restart is also different, the SVD path locks converged singular triplets and restarts on the unconverged subspace.
The realistic shared surface is the reorthogonalization helpers (CGS2/MGS2) and the cublas wrapper calls. The existing lanczos.cuh does its reorthogonalization inline against its own single-vector layout, so factoring CGS2/MGS2 into a shared utility would require touching the existing eigensolver too. I'd rather land this PR as-is and do a separate refactor PR to extract a shared bidiag_reorth /lanczos_reorth utility if you want.

@cjnolet

cjnolet commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

/ok to test b80ca30

Comment thread cpp/include/raft/sparse/solver/solver_types.hpp
Comment thread cpp/include/raft/sparse/solver/lanczos_svds.cuh
@cjnolet

cjnolet commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

/ok to test 93e9a19

@divyegala

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Lanczos-based sparse SVD support alongside the existing solver path.
    • Expanded the Python sparse SVD API with solver selection, convergence controls, and orthogonalization options.
    • Added a new sparse SVD benchmark script and GPU benchmark coverage.
  • Bug Fixes

    • Improved handling of sparse matrix input types and size limits.
    • Added validation and clearer error reporting for invalid solver or orthogonalization settings.
  • Tests

    • Added comprehensive C++ and Python coverage for accuracy, orthonormality, reconstruction quality, and non-convergence cases.

Walkthrough

Adds a Lanczos bidiagonalization SVD solver for sparse matrices in RAFT, including detail implementation, public/runtime C++ APIs, shared solver config types (replacing svds_config.hpp), CMake wiring, benchmarks, C++ tests, Python bindings for svds(solver="lanczos"), and a Python benchmarking script.

Changes

Sparse Lanczos SVD solver

Layer / File(s) Summary
Shared SVD config refactor
cpp/include/raft/sparse/solver/solver_types.hpp, cpp/include/raft/sparse/solver/svds_config.hpp, cpp/include/raft/sparse/solver/detail/svds_optional.hpp, cpp/include/raft/sparse/solver/detail/randomized_svds.cuh, cpp/include/raft/sparse/solver/randomized_svds.cuh, cpp/include/raft_runtime/solver/randomized_svds.hpp
Introduces solver_types.hpp with sparse_svd_config and sparse_lanczos_svd_config, removes svds_config.hpp, adds nondeduced optional-view helper, and updates include dependencies accordingly.
Lanczos SVD core algorithm
cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh
Implements GPU helper kernels, CGS2/MGS2 orthogonalization, restarted Lanczos bidiagonalization, small bidiagonal SVD via QR, Ritz vector locking, restart-vector computation, singular-triplet sorting, and sign correction.
Public and runtime APIs
cpp/include/raft/sparse/solver/lanczos_svds.cuh, cpp/include/raft_runtime/solver/lanczos_svds.hpp, cpp/src/raft_runtime/solver/lanczos_svds.cuh, cpp/src/raft_runtime/solver/lanczos_svds_float.cu, cpp/src/raft_runtime/solver/lanczos_svds_double.cu, cpp/CMakeLists.txt, docs/source/cpp_api/sparse_solver.rst
Adds public sparse_lanczos_svd overloads (operator and CSR view), runtime C API for float/double, and build/documentation wiring.
C++ tests and benchmarks
cpp/tests/sparse/solver/lanczos_svds.cu, cpp/tests/CMakeLists.txt, cpp/bench/prims/sparse/svds.cu, cpp/bench/prims/CMakeLists.txt
Adds GTest coverage for diagonal/clustered/hardening/rotated cases and non-convergence, plus a benchmark harness comparing Lanczos and randomized SVD on synthetic or file-backed CSR data.
Python svds bindings and tests
python/pylibraft/pylibraft/sparse/linalg/svds.pyx, python/pylibraft/pylibraft/tests/test_sparse.py
Extends svds() with solver, ncv, tol, maxiter, orthogonalization parameters, adds Cython bindings/config for the Lanczos runtime path, and adds tests for the new solver mode.
Python benchmark script
python/pylibraft/benchmarks/sparse_svds.py
Adds a standalone script benchmarking raft, rsc, CuPy, and SciPy sparse SVD solvers on CSR/H5AD data with timing and quality metrics.

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

Related issues: None specified.

Related PRs: None specified.

Suggested labels: cpp, python, feature request, non-breaking

Suggested reviewers: cjnolet, tfeher

🐇 A lanczos hop, bidiagonal and spry,
Restarts and locks singular triplets nearby,
CGS or MGS2, whichever you choose,
Benchmarks and tests all wired to amuse,
Python calls too — the rabbit says "svd, aye!"
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a sparse Lanczos SVD solver.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the new sparse Lanczos SVD solver and benchmarks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 2

🧹 Nitpick comments (6)
python/pylibraft/pylibraft/tests/test_sparse.py (1)

228-405: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer standalone test functions over class methods.

The new Lanczos tests are added as methods on class TestSvds. The repo guideline asks for standalone test_foo_bar() functions rather than class TestFoo test classes. These new tests follow the file's existing class convention, so this is non-blocking, but consider extracting them (or the whole suite) into module-level functions in a follow-up.

As per coding guidelines: "Write tests as standalone test_foo_bar() functions rather than class TestFoo test classes."

🤖 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 `@python/pylibraft/pylibraft/tests/test_sparse.py` around lines 228 - 405, The
new Lanczos tests are implemented as methods on TestSvds, but the test guideline
prefers standalone test_foo_bar() functions. Move these cases out of the class
and make them module-level tests, preserving the same assertions and parameters
in functions like test_lanczos_diagonal_spectrum,
test_lanczos_return_singular_vectors_modes,
test_lanczos_rotated_clustered_spectrum,
test_lanczos_rank_deficient_and_edge_shapes,
test_lanczos_non_convergence_raises, and test_invalid_solver.

Source: Coding guidelines

cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh (2)

251-254: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Breakdown epsilon is not type-aware.

eps = 1e-9 is fixed regardless of ValueTypeT, but float's machine epsilon (~1.19e-7) is larger than this threshold, so breakdown detection is comparatively looser/less reliable for the float instantiation than for double.

🔢 Scale epsilon by machine precision
-  ValueTypeT eps = ValueTypeT(1e-9);
+  ValueTypeT eps = std::sqrt(std::numeric_limits<ValueTypeT>::epsilon());
🤖 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/include/raft/sparse/solver/detail/lanczos_svds.cuh` around lines 251 -
254, The breakdown threshold in lanczos_svds.cuh is hardcoded in the Lanczos SVD
setup, so it is not scaled to ValueTypeT and is too small for float. Update the
epsilon initialization near the op.rows()/op.cols() setup to derive the
threshold from machine precision for the current ValueTypeT (for example via
std::numeric_limits<ValueTypeT>::epsilon() with an appropriate scale) so the
breakdown check in the Lanczos routine behaves consistently for both float and
double.

40-50: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Redundant host-device sync in the hot Lanczos step loop.

cublasnrm2 in the default host pointer mode already synchronizes internally to populate result; the following resource::sync_stream(handle, stream) on Line 48 is a second blocking barrier. Combined with the fact that normalize_or_randomize (and thus vector_norm) is invoked twice per Lanczos step (Lines 296-306, 320-331), this yields up to 2 * active_ncv explicit host syncs per restart iteration, which can materially hurt performance on large sparse matrices in this hot path.

⚡ Remove the redundant explicit sync
 template <typename ValueTypeT>
 ValueTypeT vector_norm(raft::resources const& handle, ValueTypeT const* x, int n)
 {
   common::nvtx::range<common::nvtx::domain::raft> scope("lanczos_svds::vector_norm");
   auto cublas_handle = resource::get_cublas_handle(handle);
   auto stream        = resource::get_cuda_stream(handle);
   ValueTypeT result{};
   RAFT_CUBLAS_TRY(raft::linalg::detail::cublasnrm2(cublas_handle, n, x, 1, &result, stream));
-  resource::sync_stream(handle, stream);
   return result;
 }

If the extra sync was added deliberately to guard against non-default cuBLAS stream/pointer-mode configurations, please confirm with a comment; otherwise this is safe to drop.

Also applies to: 192-215, 275-332

🤖 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/include/raft/sparse/solver/detail/lanczos_svds.cuh` around lines 40 - 50,
The `vector_norm` helper in `lanczos_svds.cuh` is adding a redundant blocking
`resource::sync_stream(handle, stream)` after
`raft::linalg::detail::cublasnrm2`, which already completes before `result` is
read in the default host pointer mode. Remove that extra sync from `vector_norm`
and keep the rest of the Lanczos flow (`normalize_or_randomize` call sites)
unchanged unless there is a documented need for a non-default cuBLAS mode; if
so, add a brief comment near `vector_norm` explaining the requirement.

Source: Coding guidelines

cpp/bench/prims/sparse/svds.cu (1)

312-371: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate benchmark classes differ by a single config line.

lanczos_svds_bench and lanczos_mgs2_svds_bench are otherwise identical. Consider parameterizing a single class with a constructor/bool flag instead of duplicating the full run_benchmark body.

♻️ Proposed consolidation
 template <typename value_t>
-class lanczos_svds_bench : public svds_bench_base<value_t> {
+class lanczos_svds_bench : public svds_bench_base<value_t> {
  public:
-  using svds_bench_base<value_t>::svds_bench_base;
+  explicit lanczos_svds_bench(svds_input const& p, bool use_mgs2 = false)
+    : svds_bench_base<value_t>(p), use_mgs2_(use_mgs2)
+  {
+  }

   void run_benchmark(::benchmark::State& state) override
   {
     auto csr_matrix = this->csr_view();

     raft::sparse::solver::sparse_lanczos_svd_config<value_t> config;
     config.n_components   = this->params.k;
     config.ncv            = this->params.ncv;
     config.tolerance      = value_t(1e-4);
     config.max_iterations = this->params.max_iterations;
     config.seed           = 1234;
+    config.use_mgs2_orthogonalization = use_mgs2_;

     this->loop_on_state(
       state,
       [&]() {
         raft::sparse::solver::sparse_lanczos_svd(this->handle,
                                                  config,
                                                  csr_matrix,
                                                  this->singular_values.view(),
                                                  this->U.view(),
                                                  this->Vt.view());
       },
       false);
   }
+
+ private:
+  bool use_mgs2_;
 };
-
-template <typename value_t>
-class lanczos_mgs2_svds_bench : public svds_bench_base<value_t> {
- public:
-  using svds_bench_base<value_t>::svds_bench_base;
-  ...
-};
🤖 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/bench/prims/sparse/svds.cu` around lines 312 - 371, Consolidate the
duplicated benchmark logic in lanczos_svds_bench and lanczos_mgs2_svds_bench by
extracting the shared run_benchmark flow into one implementation and
parameterizing the orthogonalization choice with a constructor flag or similar
field. Keep the common sparse_lanczos_svd_config setup in one place, only
toggling config.use_mgs2_orthogonalization for the MGS2 variant, and preserve
the existing loop_on_state and sparse_lanczos_svd call sites.
cpp/tests/sparse/solver/lanczos_svds.cu (2)

88-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for U/Vt = std::nullopt.

sparse_lanczos_svd_config documents std::nullopt as a valid way to skip storing U/Vt (per solver_types.hpp), but no test in this file exercises that path. Given RAFT's testing guideline to maintain high coverage of public APIs, consider adding a case that passes std::nullopt for one or both outputs.

As per coding guidelines, "It's important for RAFT to maintain a high test coverage of the public APIs in order to minimize the potential for downstream projects to encounter unexpected build or runtime behavior."

Also applies to: 205-241, 315-330

🤖 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_svds.cu` around lines 88 - 125, Add a test
case in lanczos_svds.cu that exercises sparse_lanczos_svd with std::nullopt for
U and/or Vt, since sparse_lanczos_svd_config and the solver API support skipping
these outputs. Update the Run helper or add a new helper around
sparse_lanczos_svd to call it with nullopt, then verify the singular values
still match expectations without materializing the omitted outputs. Use the
existing sparse_lanczos_svd, sparse_lanczos_svd_config, and Run symbols to keep
the new coverage aligned with the current test structure.

Source: Coding guidelines


25-544: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting shared test harness.

The CSR buffer setup → config construction → solve → host copy-back → sync pattern is repeated near-identically across LanczosSvdsTest::Run, LanczosClusteredSpectrumTest::Run, run_diagonal_hardening_case, and run_rotated_block_hardening_case. Extracting a common helper (e.g., taking indptr/indices/values + config and returning host S/U/Vt) would reduce duplication and ease future maintenance of this test suite.

🤖 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_svds.cu` around lines 25 - 544, The test file
repeats the same CSR setup, sparse_lanczos_svd config construction, solve, host
copy-back, and stream sync flow in LanczosSvdsTest::Run,
LanczosClusteredSpectrumTest::Run, run_diagonal_hardening_case, and
run_rotated_block_hardening_case. Extract that sequence into a shared helper
that accepts the CSR buffers and sparse_lanczos_svd_config and returns the host
S/U/Vt results, then update each of those call sites to use it while keeping
only the case-specific assertions and matrix construction in place.
🤖 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/include/raft/sparse/solver/detail/lanczos_svds.cuh`:
- Around line 124-162: The cuBLAS pointer mode in mgs2_orthogonalize is only
restored on the success path, so an exception from any RAFT_CUBLAS_TRY or
RAFT_CUDA_TRY can leave the shared cublas handle in device mode. Update
mgs2_orthogonalize to use an exception-safe scoped guard, or reuse an existing
RAFT canonical pointer-mode RAII helper if one exists, so the handle is always
returned to CUBLAS_POINTER_MODE_HOST on every exit path. Keep the existing
cublasSetPointerMode setup and ensure the restore happens automatically even if
the loop aborts early.

In `@cpp/include/raft/sparse/solver/lanczos_svds.cuh`:
- Around line 1-16: Move the Lanczos SVDS implementation templates out of the
public header by splitting the existing wrapper in `lanczos_svds.cuh` into the
usual inline/explicit-instantiation pair. Keep only the public wrapper
declarations in the header, move the template definitions from
`detail/lanczos_svds.cuh` into an `-inl.cuh`/`-ext.cuh` layout, and add
`RAFT_COMPILED` / `RAFT_EXPLICIT_INSTANTIATE_ONLY` guards so downstream
translation units do not instantiate the implementation directly. Make sure the
wrapper overloads remain the only public entry points and that the float/double
explicit instantiations used by the `.cu` files are reused.

---

Nitpick comments:
In `@cpp/bench/prims/sparse/svds.cu`:
- Around line 312-371: Consolidate the duplicated benchmark logic in
lanczos_svds_bench and lanczos_mgs2_svds_bench by extracting the shared
run_benchmark flow into one implementation and parameterizing the
orthogonalization choice with a constructor flag or similar field. Keep the
common sparse_lanczos_svd_config setup in one place, only toggling
config.use_mgs2_orthogonalization for the MGS2 variant, and preserve the
existing loop_on_state and sparse_lanczos_svd call sites.

In `@cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh`:
- Around line 251-254: The breakdown threshold in lanczos_svds.cuh is hardcoded
in the Lanczos SVD setup, so it is not scaled to ValueTypeT and is too small for
float. Update the epsilon initialization near the op.rows()/op.cols() setup to
derive the threshold from machine precision for the current ValueTypeT (for
example via std::numeric_limits<ValueTypeT>::epsilon() with an appropriate
scale) so the breakdown check in the Lanczos routine behaves consistently for
both float and double.
- Around line 40-50: The `vector_norm` helper in `lanczos_svds.cuh` is adding a
redundant blocking `resource::sync_stream(handle, stream)` after
`raft::linalg::detail::cublasnrm2`, which already completes before `result` is
read in the default host pointer mode. Remove that extra sync from `vector_norm`
and keep the rest of the Lanczos flow (`normalize_or_randomize` call sites)
unchanged unless there is a documented need for a non-default cuBLAS mode; if
so, add a brief comment near `vector_norm` explaining the requirement.

In `@cpp/tests/sparse/solver/lanczos_svds.cu`:
- Around line 88-125: Add a test case in lanczos_svds.cu that exercises
sparse_lanczos_svd with std::nullopt for U and/or Vt, since
sparse_lanczos_svd_config and the solver API support skipping these outputs.
Update the Run helper or add a new helper around sparse_lanczos_svd to call it
with nullopt, then verify the singular values still match expectations without
materializing the omitted outputs. Use the existing sparse_lanczos_svd,
sparse_lanczos_svd_config, and Run symbols to keep the new coverage aligned with
the current test structure.
- Around line 25-544: The test file repeats the same CSR setup,
sparse_lanczos_svd config construction, solve, host copy-back, and stream sync
flow in LanczosSvdsTest::Run, LanczosClusteredSpectrumTest::Run,
run_diagonal_hardening_case, and run_rotated_block_hardening_case. Extract that
sequence into a shared helper that accepts the CSR buffers and
sparse_lanczos_svd_config and returns the host S/U/Vt results, then update each
of those call sites to use it while keeping only the case-specific assertions
and matrix construction in place.

In `@python/pylibraft/pylibraft/tests/test_sparse.py`:
- Around line 228-405: The new Lanczos tests are implemented as methods on
TestSvds, but the test guideline prefers standalone test_foo_bar() functions.
Move these cases out of the class and make them module-level tests, preserving
the same assertions and parameters in functions like
test_lanczos_diagonal_spectrum, test_lanczos_return_singular_vectors_modes,
test_lanczos_rotated_clustered_spectrum,
test_lanczos_rank_deficient_and_edge_shapes,
test_lanczos_non_convergence_raises, and test_invalid_solver.
🪄 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: bf8109ed-b790-4078-9385-89d9febf71c1

📥 Commits

Reviewing files that changed from the base of the PR and between 0903417 and 93e9a19.

📒 Files selected for processing (21)
  • cpp/CMakeLists.txt
  • cpp/bench/prims/CMakeLists.txt
  • cpp/bench/prims/sparse/svds.cu
  • cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh
  • cpp/include/raft/sparse/solver/detail/randomized_svds.cuh
  • cpp/include/raft/sparse/solver/detail/svds_optional.hpp
  • cpp/include/raft/sparse/solver/lanczos_svds.cuh
  • cpp/include/raft/sparse/solver/randomized_svds.cuh
  • cpp/include/raft/sparse/solver/solver_types.hpp
  • cpp/include/raft/sparse/solver/svds_config.hpp
  • cpp/include/raft_runtime/solver/lanczos_svds.hpp
  • cpp/include/raft_runtime/solver/randomized_svds.hpp
  • cpp/src/raft_runtime/solver/lanczos_svds.cuh
  • cpp/src/raft_runtime/solver/lanczos_svds_double.cu
  • cpp/src/raft_runtime/solver/lanczos_svds_float.cu
  • cpp/tests/CMakeLists.txt
  • cpp/tests/sparse/solver/lanczos_svds.cu
  • docs/source/cpp_api/sparse_solver.rst
  • python/pylibraft/benchmarks/sparse_svds.py
  • python/pylibraft/pylibraft/sparse/linalg/svds.pyx
  • python/pylibraft/pylibraft/tests/test_sparse.py
💤 Files with no reviewable changes (1)
  • cpp/include/raft/sparse/solver/svds_config.hpp

Comment on lines +124 to +162
template <typename ValueTypeT>
void mgs2_orthogonalize(raft::resources const& handle,
ValueTypeT* target,
ValueTypeT const* basis,
int n_rows,
int n_valid,
ValueTypeT* coeffs)
{
common::nvtx::range<common::nvtx::domain::raft> scope("lanczos_svds::mgs2_orthogonalize");
if (n_valid <= 0) { return; }

auto cublas_handle = resource::get_cublas_handle(handle);
auto stream = resource::get_cuda_stream(handle);

RAFT_CUBLAS_TRY(cublasSetPointerMode(cublas_handle, CUBLAS_POINTER_MODE_DEVICE));
for (int pass = 0; pass < 2; ++pass) {
for (int j = 0; j < n_valid; ++j) {
auto const* q_j = basis + static_cast<std::size_t>(j) * n_rows;
auto* coeff = coeffs + j;
{
common::nvtx::range<common::nvtx::domain::raft> dot_scope("lanczos_svds::mgs2_dot");
RAFT_CUBLAS_TRY(
raft::linalg::detail::cublasdot(cublas_handle, n_rows, q_j, 1, target, 1, coeff, stream));
}
{
common::nvtx::range<common::nvtx::domain::raft> negate_scope("lanczos_svds::mgs2_negate");
negate_scalar_kernel<<<1, 1, 0, stream>>>(coeff);
RAFT_CUDA_TRY(cudaPeekAtLastError());
}
{
common::nvtx::range<common::nvtx::domain::raft> subtract_scope(
"lanczos_svds::mgs2_subtract");
RAFT_CUBLAS_TRY(raft::linalg::detail::cublasaxpy(
cublas_handle, n_rows, coeff, q_j, 1, target, 1, stream));
}
}
}
RAFT_CUBLAS_TRY(cublasSetPointerMode(cublas_handle, CUBLAS_POINTER_MODE_HOST));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore cuBLAS pointer mode via RAII, not only on the happy path.

If any RAFT_CUBLAS_TRY/RAFT_CUDA_TRY call inside the loop throws, cublasSetPointerMode(..., CUBLAS_POINTER_MODE_HOST) on Line 161 never executes, leaving the shared raft::resources cuBLAS handle stuck in device-pointer mode. Every other cuBLAS call in this file (and elsewhere sharing the same handle) assumes host pointer mode for scalar args, so a later failure here would silently corrupt subsequent computations rather than just aborting cleanly.

As per coding guidelines, "Ensure device memory, streams, and events are cleaned up on all paths; avoid GPU memory leaks, CUDA stream/event leaks, and missing RAII/exception-safe cleanup."

🔒 Proposed RAII guard for cuBLAS pointer mode
+struct cublas_pointer_mode_guard {
+  cublasHandle_t handle_;
+  cublasPointerMode_t prev_mode_;
+  cublas_pointer_mode_guard(cublasHandle_t handle, cublasPointerMode_t mode) : handle_(handle)
+  {
+    RAFT_CUBLAS_TRY(cublasGetPointerMode(handle_, &prev_mode_));
+    RAFT_CUBLAS_TRY(cublasSetPointerMode(handle_, mode));
+  }
+  ~cublas_pointer_mode_guard() { cublasSetPointerMode(handle_, prev_mode_); }
+};
+
 template <typename ValueTypeT>
 void mgs2_orthogonalize(raft::resources const& handle,
                         ValueTypeT* target,
                         ValueTypeT const* basis,
                         int n_rows,
                         int n_valid,
                         ValueTypeT* coeffs)
 {
   common::nvtx::range<common::nvtx::domain::raft> scope("lanczos_svds::mgs2_orthogonalize");
   if (n_valid <= 0) { return; }

   auto cublas_handle = resource::get_cublas_handle(handle);
   auto stream        = resource::get_cuda_stream(handle);

-  RAFT_CUBLAS_TRY(cublasSetPointerMode(cublas_handle, CUBLAS_POINTER_MODE_DEVICE));
+  cublas_pointer_mode_guard pointer_mode_guard(cublas_handle, CUBLAS_POINTER_MODE_DEVICE);
   for (int pass = 0; pass < 2; ++pass) {
     ...
   }
-  RAFT_CUBLAS_TRY(cublasSetPointerMode(cublas_handle, CUBLAS_POINTER_MODE_HOST));
 }

Please verify whether RAFT already has a canonical scoped pointer-mode guard elsewhere that should be reused instead:

#!/bin/bash
rg -n -C3 'cublasSetPointerMode|CUBLAS_POINTER_MODE' cpp/include cpp/src
🤖 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/include/raft/sparse/solver/detail/lanczos_svds.cuh` around lines 124 -
162, The cuBLAS pointer mode in mgs2_orthogonalize is only restored on the
success path, so an exception from any RAFT_CUBLAS_TRY or RAFT_CUDA_TRY can
leave the shared cublas handle in device mode. Update mgs2_orthogonalize to use
an exception-safe scoped guard, or reuse an existing RAFT canonical pointer-mode
RAII helper if one exists, so the handle is always returned to
CUBLAS_POINTER_MODE_HOST on every exit path. Keep the existing
cublasSetPointerMode setup and ensure the restore happens automatically even if
the loop aborts early.

Source: Coding guidelines

Comment on lines +1 to +16
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include <raft/core/device_csr_matrix.hpp>
#include <raft/core/device_mdspan.hpp>
#include <raft/core/resources.hpp>
#include <raft/sparse/solver/detail/csr_linear_operator.cuh>
#include <raft/sparse/solver/detail/lanczos_svds.cuh>
#include <raft/sparse/solver/detail/svds_optional.hpp>

#include <optional>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether sibling solver headers (randomized_svds, eigensolver lanczos) follow the split-header pattern,
# to confirm whether this PR's lanczos_svds.cuh is a new deviation or matches existing convention.
fd -e cuh -e hpp . cpp/include/raft/sparse/solver cpp/include/raft/sparse/solver/detail
rg -n "RAFT_EXPLICIT_INSTANTIATE_ONLY|RAFT_COMPILED|extern template" cpp/include/raft/sparse/solver/randomized_svds.cuh cpp/include/raft/sparse/solver/detail/randomized_svds.cuh 2>/dev/null

Repository: NVIDIA/raft

Length of output: 1595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the public and detail headers for Lanczos SVDS and nearby solver headers.
for f in \
  cpp/include/raft/sparse/solver/lanczos_svds.cuh \
  cpp/include/raft/sparse/solver/lanczos.cuh \
  cpp/include/raft/sparse/solver/randomized_svds.cuh \
  cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh \
  cpp/include/raft/sparse/solver/detail/lanczos.cuh \
  cpp/include/raft/sparse/solver/detail/randomized_svds.cuh
do
  echo "===== $f ====="
  wc -l "$f"
done

echo
echo "===== public header excerpts ====="
sed -n '1,220p' cpp/include/raft/sparse/solver/lanczos_svds.cuh
echo
sed -n '1,220p' cpp/include/raft/sparse/solver/lanczos.cuh
echo
sed -n '1,220p' cpp/include/raft/sparse/solver/randomized_svds.cuh

echo
echo "===== detail header excerpts ====="
sed -n '1,260p' cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh
echo
sed -n '1,260p' cpp/include/raft/sparse/solver/detail/lanczos.cuh
echo
sed -n '1,260p' cpp/include/raft/sparse/solver/detail/randomized_svds.cuh

echo
echo "===== explicit instantiation files ====="
fd -a -e cu -e cuh -e cpp 'lanczos_svds|randomized_svds|lanczos' cpp/src cpp/include | sort
rg -n "explicit instantiation|extern template|RAFT_EXPLICIT_INSTANTIATE_ONLY|RAFT_COMPILED|template class|template void|template struct" cpp/src cpp/include/raft/sparse/solver -g '*lanczos*' -g '*randomized_svds*'

Repository: NVIDIA/raft

Length of output: 47320


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the files that were referenced by the initial results to identify how this area is organized.
fd -a -e cu -e cuh -e cpp 'lanczos_svds|randomized_svds|lanczos' cpp/src cpp/include | sort
rg -n "RAFT_EXPLICIT_INSTANTIATE_ONLY|RAFT_COMPILED|extern template|template class|template void|template struct" \
  cpp/src cpp/include/raft/sparse/solver \
  -g '*lanczos*' -g '*randomized_svds*'

Repository: NVIDIA/raft

Length of output: 1355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files and search for split-header patterns in this area.
printf "\n-- file list --\n"
git ls-files \
  'cpp/include/raft/sparse/solver/*' \
  'cpp/include/raft/sparse/solver/detail/*' \
  'cpp/src/raft_runtime/solver/*' | \
  rg 'lanczos|randomized_svds|svds'

printf "\n-- public headers --\n"
for f in \
  cpp/include/raft/sparse/solver/lanczos_svds.cuh \
  cpp/include/raft/sparse/solver/randomized_svds.cuh \
  cpp/include/raft/sparse/solver/lanczos.cuh
do
  echo "### $f"
  sed -n '1,220p' "$f"
  echo
done

printf "\n-- detail headers --\n"
for f in \
  cpp/include/raft/sparse/solver/detail/lanczos_svds.cuh \
  cpp/include/raft/sparse/solver/detail/randomized_svds.cuh \
  cpp/include/raft/sparse/solver/detail/lanczos.cuh
do
  echo "### $f"
  sed -n '1,260p' "$f"
  echo
done

printf "\n-- explicit instantiation files --\n"
for f in \
  cpp/src/raft_runtime/solver/lanczos_svds_float.cu \
  cpp/src/raft_runtime/solver/lanczos_svds_double.cu \
  cpp/src/raft_runtime/solver/randomized_svds_float.cu \
  cpp/src/raft_runtime/solver/randomized_svds_double.cu
do
  if [ -f "$f" ]; then
    echo "### $f"
    sed -n '1,220p' "$f"
    echo
  fi
done

printf "\n-- search for RAFT_COMPILED / RAFT_EXPLICIT_INSTANTIATE_ONLY / extern template --\n"
rg -n "RAFT_COMPILED|RAFT_EXPLICIT_INSTANTIATE_ONLY|extern template" \
  cpp/include/raft/sparse/solver cpp/src/raft_runtime/solver

Repository: NVIDIA/raft

Length of output: 48062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the runtime wrapper headers and any nearby instantiation plumbing.
for f in \
  cpp/src/raft_runtime/solver/lanczos_svds.cuh \
  cpp/src/raft_runtime/solver/randomized_svds.cuh \
  cpp/src/raft_runtime/solver/lanczos_solver.cuh
do
  echo "===== $f ====="
  wc -l "$f"
  sed -n '1,240p' "$f"
  echo
done

echo "===== search for FUNC_DEF / explicit instantiation plumbing ====="
rg -n "FUNC_DEF|RAFT_EXPLICIT_INSTANTIATE_ONLY|RAFT_COMPILED|extern template|template class|template void" cpp/src/raft_runtime/solver cpp/include/raft/sparse/solver

Repository: NVIDIA/raft

Length of output: 7292


Split Lanczos SVDS templates out of the public header. cpp/include/raft/sparse/solver/lanczos_svds.cuh still includes detail/lanczos_svds.cuh directly, so downstream TUs will instantiate the implementation even though the runtime .cu files only instantiate the wrapper overloads. Add the usual -inl.cuh/-ext.cuh split with RAFT_COMPILED / RAFT_EXPLICIT_INSTANTIATE_ONLY guards so the prebuilt float/double instantiations can be reused.

🤖 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/include/raft/sparse/solver/lanczos_svds.cuh` around lines 1 - 16, Move
the Lanczos SVDS implementation templates out of the public header by splitting
the existing wrapper in `lanczos_svds.cuh` into the usual
inline/explicit-instantiation pair. Keep only the public wrapper declarations in
the header, move the template definitions from `detail/lanczos_svds.cuh` into an
`-inl.cuh`/`-ext.cuh` layout, and add `RAFT_COMPILED` /
`RAFT_EXPLICIT_INSTANTIATE_ONLY` guards so downstream translation units do not
instantiate the implementation directly. Make sure the wrapper overloads remain
the only public entry points and that the float/double explicit instantiations
used by the `.cu` files are reused.

Source: Path instructions

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

Labels

feature request New feature or request non-breaking Non-breaking change

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

5 participants