Skip to content

Size the BlockLanczos thick restart by R_size instead of the initial block size - #172

Open
leburgel wants to merge 2 commits into
Jutho:masterfrom
leburgel:lb/blocklanczos_restart_rsize
Open

leburgel wants to merge 2 commits into
Jutho:masterfrom
leburgel:lb/blocklanczos_restart_rsize

Conversation

@leburgel

@leburgel leburgel commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

When looking into why my attempted warm-start of BlockLanczos in #170 still required more applications than a cold start, I stumbled into some inconsistencies in the shrinking procedure compared to a regular Lanczos run.

The BlockLanczos restart used the initial block size bs to set how many vectors to keep and which rows of U couple to the residual. It should use the current residual block size fact.R_size, which becomes smaller than bs whenever block_qr! drops vectors. This PR switches the shrink branch to R_size.

The restarted factorization was already correct. The extra rows of U don't couple to the residual and don't change the kept subspace. But keep was rounded down to a multiple of bs. For example, with bs = 6 and krylovdim = 20, it stayed at 12, whereas Lanczos keeps 12 to 14 as vectors converge. With this change, a block that has shrunk to R_size = 1 restarts the same way as Lanczos. When R_size == bs, the first commit changes nothing; see the second commit below.

The reproducer below starts from five exact eigenvectors plus a random vector, so R_size drops to 1 after the first step:

using KrylovKit, LinearAlgebra, Random
Random.seed!(1)
n = 4000
A = SymTridiagonal(2 .+ 0.5 .* rand(n), -ones(n - 1))
opts = (; tol = 1e-10, ishermitian = true, verbosity = 0, maxiter = 10_000)

E = eigen(A, 1:5).vectors
v = rand(n)
x₀ = Block([[E[:, i] for i in 1:5]; [v]])

for kd in (20, 30, 60)
    lanczos = eigsolve(A, v, 6, :SR; opts..., krylovdim = kd)[3]
    block = eigsolve(A, copy(x₀), 6, :SR; opts..., krylovdim = kd)[3]
    println("krylovdim = $kd: Lanczos(v) numops = $(lanczos.numops), BlockLanczos(x₀) numops = $(block.numops), converged = $(block.converged)")
end
krylovdim Lanczos(v) BlockLanczos (master) BlockLanczos (this PR)
20 905 1229 993
30 633 535 541
60 555 445 435

This matters most together with #170. Take a warm start from the output vectors of an earlier unconverged run at krylovdim = 20: it needs 1173 matvecs with #170 alone and 749 with both PRs, compared with 886 for a cold start.

Second commit: never keep fewer vectors than have converged. Lanczos keeps div(3 * krylovdim + 2 * converged, 5) vectors, which always satisfies converged <= keep < krylovdim. Rounding that down to a multiple of R_size can take it below converged, and the restart then throws away Ritz vectors that had already converged. This needs a residual block that stays large while many vectors converge, which typically means degenerate eigenvalues, and krylovdim - converged < 5(R_size - 1)/3. With the default krylovdim = 100 and R_size = 16, that only happens once more than 75 vectors have converged. In that case the restart now falls back to the unrounded Lanczos value. Otherwise nothing changes, and the reproducer above gives identical numbers.

using KrylovKit, LinearAlgebra, Random
BLAS.set_num_threads(1)  # reproducible numbers
Random.seed!(2)
n, b, mult = 240, 16, 16
λ = repeat(vcat(1.0:5.0, 200 .+ 10 .* rand(10)), inner = mult)  # 15 distinct eigenvalues, each 16-fold
Q = Matrix(qr(randn(n, n)).Q)
A = Hermitian(Q * Diagonal(λ) * Q')
x₀ = Block([randn(n) for _ in 1:b])
alg = BlockLanczos(; krylovdim = 40, maxiter = 300, tol = 1e-10)
_, _, info = eigsolve(A, x₀, 32, :SR, alg)
@show info.converged info.numiter info.numops
converged numiter numops
first commit only 32 218 4271
both commits 32 138 3625

Without the second commit, restart 111 has converged = 28 with R_size = 12, which gives keep = 24, and the next restart counts only 24 converged.

@leburgel
leburgel force-pushed the lb/blocklanczos_restart_rsize branch from 35e4422 to 17acdcb Compare September 30, 2026 18:30
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.65%. Comparing base (531b074) to head (17acdcb).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #172   +/-   ##
=======================================
  Coverage   86.65%   86.65%           
=======================================
  Files          36       36           
  Lines        3918     3919    +1     
=======================================
+ Hits         3395     3396    +1     
  Misses        523      523           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant