Skip to content

perf: DMRG(2) with smarter adaptive tolerances#455

Open
lkdvos wants to merge 15 commits into
mainfrom
ld-dynamictols
Open

perf: DMRG(2) with smarter adaptive tolerances#455
lkdvos wants to merge 15 commits into
mainfrom
ld-dynamictols

Conversation

@lkdvos

@lkdvos lkdvos commented Jul 10, 2026

Copy link
Copy Markdown
Member

This PR features a somewhat large refactor of the DMRG implementations, and finally fixes #311 hopefully in a more systematic way.
The main idea is that I want to use loose bounds on the krylov solvers, since currently everyone has the wrong assumption that ITensors is a lot faster than MPSKit, which is just due to the default settings.
Nevertheless, while for ITensorMPS you just have to know and read the docs to figure out that if you want to actually converge something for a non-gapped state, I wanted to keep some guarantees on stability and convergence rates.
Therefore, this PR introduces a bunch more knobs to determine adaptive tolerance and krylov settings based on the information that is available:

  1. The tolerance of the krylov solver depends on the global convergence, and oversolving locally (especially for finite systems) has very little benefit.
  2. The tolerance of the krylov solver should not go far below the truncation error incurred in adaptive DMRG schemes, since that error will dominate the eigenvector precision anyways.
  3. The gap of the local effective system can be predicted by investigating the decay rate of the previous sweep, which is theoretically predicted to be geometric for Lanczos solvers. Using this, we can make an educated guess for how gapped the system is and adapt accordingly.

A secondary part of this PR is just me refactoring the DMRG implementations into a more readable and consolidated form, which I think overall just improves the code quailty.


Will still attempt to post some actual benchmarks here comparing main/this branch, as well as some comparisons with ITensorMPS.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.39548% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/states/orthoview.jl 82.85% 6 Missing ⚠️
src/utility/dynamictols.jl 85.71% 4 Missing ⚠️
src/algorithms/fixedpoint.jl 40.00% 3 Missing ⚠️
src/algorithms/approximate/vomps.jl 75.00% 1 Missing ⚠️
src/algorithms/groundstate/dmrg.jl 98.38% 1 Missing ⚠️
src/algorithms/statmech/vomps.jl 80.00% 1 Missing ⚠️
src/states/finitemps.jl 83.33% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/MPSKit.jl 100.00% <ø> (ø)
src/algorithms/approximate/fvomps.jl 91.30% <100.00%> (ø)
src/algorithms/approximate/idmrg.jl 96.72% <100.00%> (ø)
src/algorithms/changebonds/optimalexpand.jl 98.73% <100.00%> (ø)
src/algorithms/changebonds/randexpand.jl 64.38% <100.00%> (ø)
src/algorithms/changebonds/sketchedexpand.jl 98.43% <100.00%> (ø)
src/algorithms/excitation/dmrgexcitation.jl 100.00% <100.00%> (ø)
src/algorithms/groundstate/idmrg.jl 99.35% <100.00%> (ø)
src/algorithms/groundstate/vumps.jl 81.31% <100.00%> (ø)
src/algorithms/statmech/idmrg.jl 97.72% <100.00%> (ø)
... and 10 more

... and 11 files with indirect coverage changes

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

@lkdvos
lkdvos force-pushed the ld-dynamictols branch 2 times, most recently from 8e22df4 to 8ab7f54 Compare July 12, 2026 00:06
@lkdvos
lkdvos marked this pull request as ready for review July 12, 2026 00:06
@lkdvos

lkdvos commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Here I've added some results on the comparisons for the sweeptimes, but also on the energy reached after a given walltime, in an attempt to not just measure what the fastest way is to get 10 sweeps done.
What's interesting is that we now consistently outperform ITensorMPS, even without symmetries, but that probably we will have to investigate the single-site version a bit more, since that seems to not perform the way I'd expect.

I'd also say that it seems really hard to make generalizations about the actual walltimes it takes to get to a given energy, since the faster sweeps get compensated with the necessity for more sweeps. In general, this is not faster everywhere, but I think it at least feels faster because the sweeps take a little less time, and it makes a naive benchmark no longer favor the ITensor version that much.

sweeptime_heisenberg_s1 energy_heisenberg_s1 energy_heisenberg_s1_2 sweeptime_heisenberg_s1_2

@borisdevos borisdevos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is amazing work, Lukas! The benchmarks also look very promising.

I was just wondering whether we should be more verbose about how AdaptiveKrylov is now the default everywhere. Also, this is not being compared explicitly in our tests by name, though I'm not sure what the criteria would be.

The refactoring indeed makes the code much easier to follow! The title of the PR could also added "performance" arguably ;)

You should definitely let others review this PR as well, since I'm no expert really, but hopefully I brought up some things with substance.

Comment thread src/algorithms/fixedpoint.jl
Comment thread src/algorithms/groundstate/dmrg.jl
Comment thread src/algorithms/groundstate/dmrg.jl
Comment thread src/states/orthoview.jl
Comment thread src/states/finitemps.jl
Comment thread src/utility/dynamictols.jl Outdated
Comment on lines +188 to +193
function instantiate_algorithm(
alg::DynamicTol, decay_rate::Real, g_local::Real, g_global::Real, eps_trunc::Real
)
tol = clamp(g_global * alg.tol_factor, alg.tol_min, alg.tol_max)
return _updatetol(alg.alg, tol)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This behavior is very different to what the user would previously expect from DynamicTol, no? I think someone can write DMRG(; alg_eigsolve = DynamicTol(Lanczos())), and then expect line 65's formula to still hold (using 1/sqrt(iter)).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point, I somehow forgot about that since my intention was to completely supersede the DynamicTol implementation. I'll make sure to restore this though, thanks!

Comment on lines +157 to +168
HAC2 = normalize!(Heff * ac2)
AC2′ = copy(HAC2)
project_complement!(AC2′, ψ.AL[pos])
project_complement_right!(AC2′, _transpose_tail(ψ.AR[pos + 1]))
ϵ_local = norm(AC2′)

# 1. local two-site update
alg_eigsolve = instantiate_algorithm(alg.alg_eigsolve, decay_rate, ϵ_local, ϵ_global, ϵ_trunc)
newA2center, info = @timeit timeroutput "AC2_eigsolve" begin
_, newA2center, info = fixedpoint(Heff, HAC2, :SR, alg_eigsolve)
(newA2center, info)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since H * AC2 is used as an initial vector to the fixed point equation, this one application should also count to the number of matrix-vector multiplications, but this isn't being logged. Maybe this 1 matvec doesn't matter in the grand scheme of things though 🤷

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It definitely does, we are now doing on the order of 5-10 of them so it is 10-20%. However, the debug information is mostly for inspecting the eigsolve behavior, since that is also what is configured with the adaptive tolerances (krylovdim=3 would end up doing 4 matvecs if we count the initial one), so I think debug-wise this is probably fine?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it useful to have H * AC2 instead of just AC2 as a starting vector? For transfer matrix fixed points, this should be a strictly better initial guess, but I am a bit worried for Hamiltonians which are such that the ground state energy is (close to) zero, and so H * AC2 might actually project out the most relevant contribution to the eigenvector when you are close to convergence.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh you are right, I was definitely thinking of the transfer matrix case.

@lkdvos

lkdvos commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Thanks a lot @borisdevos for taking the time to go over this, the feedback is definitely appreciated!

Some more things I would love inputs on:

  • Looking at the benchmarks right now, it is actually not entirely clear that these looser defaults are really helping. Of course, if the metric is to get fast sweeps this makes them a lot faster, but if the metric is to get to the groundstate faster, this doesn't seem to really be the case, especially in the gapped case or for higher bond dimensions. I might redo an actual hyperparameter sweep in the current implementation and see if I can actually get some more quantitative data on this.
  • I really don't know what to do with the convergence criterion. The galerkin error seems more principled to me, but especially in the twosite context I'm not even sure if I'm defining this correctly, since the correct thing might be to project onto the twosite left nullspace? Maybe @Jutho can say something more about this. Ultimately I'm happy to in the future work towards making this configurable, but I don't want to tackle that in this PR.
  • I have a bunch more tricks up my sleeve to make this faster, which mostly have to do with the way the environments are constructed and stored, which arguably will make a much larger difference across the board than this hyperparameter tweaking, so I don't want to spend too much time dwelling on this. I'm probably going to try and add some of these benchmark results to the documentation, where for the comparison with ITensor I will just use the "fixed krylov" settings, which is also what TenPy settled on for their benchmarks.
  • The conv vs galerkin errors currently have a different scale - the former is 1 - dot(x, y) while the latter is norm = sqrt(dot(...)). However, if I naively square root the former the behavior doesn't really fix my excitations immediately converging issue...

Comment thread src/algorithms/groundstate/dmrg.jl Outdated
Comment thread src/algorithms/groundstate/dmrg.jl
Comment thread src/algorithms/groundstate/dmrg.jl
krylovdim = iszero(ρ) ? alg.krylovdim_min : alg.krylovdim_max
maxiter = iszero(ρ) ? alg.iter_min : alg.iter_max
return (T ? Lanczos : Arnoldi)(; alg.orth, krylovdim, maxiter, tol, alg.eager, alg.verbosity)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With decay_rate = clamp((first(info.normres) / ϵ_local)^(1 / max(1, info.numops)), 1.0e-3, 0.999) above, can ρ ever fall outside (or even reach) the limits 0.0 or 1.0?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, the very first iteration I don't have any history so I set it to 0 to detect this. I'll add a comment here to clarify!

@lkdvos lkdvos changed the title refactor: DMRG(2) with smarter adaptive tolerances perf: DMRG(2) with smarter adaptive tolerances Jul 16, 2026
lkdvos and others added 15 commits July 16, 2026 17:21
Switch the DMRG/DMRG2 stop test from the overlap-based convergence error
`ϵ_conv = 1 - |⟨old|new⟩|` to the Galerkin error (already computed each local
update as `ϵ_local`/`ϵ_global`), matching VUMPS/iDMRG. Drop the now-unused
`ϵ_conv` bookkeeping from both `local_update!` methods and `find_groundstate!`.

Perturb the default `FiniteExcited` initial guess with a small per-site random
component. Under the Galerkin criterion an unperturbed ground-state guess can
already be (near) an eigenvector of the shifted operator, so convergence would
trigger on the first sweep and return the ground state instead of climbing to
the excited state. The perturbation preserves spaces (symmetry sector) and bond
dimensions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DMRG2 reused `HAC2 = Heff * ac2` (needed for the Galerkin `ϵ_local`) as the
eigensolver seed. Seeding with H*AC is power-iteration intuition (it amplifies
the largest-magnitude eigenvector), which is wrong for `:SR` (smallest
algebraic): it de-weights the ground state, and when the effective ground
eigenvalue is 0 it annihilates the ground-state component exactly. Since H
preserves the ground state's orthogonal complement, the Krylov solve then
converges to the smallest nonzero eigenvalue instead — the wrong state. This
bites frustration-free/parent Hamiltonians and any H - E0 shift.

Seed with the plain two-site center `ac2` instead, consistent with single-site
DMRG's `ac_old`. `HAC2` is still computed for the Galerkin gradient only.

Verified on a finite TFIM (L=16, g=1) shifted so E0=0: old seeding gives a
non-convergent 4.73 (ϵ_conv≈0.88), fixed seeding gives 5e-9≈0 (ϵ_conv≈2e-15);
unshifted DMRG2 still matches single-site DMRG to 1e-14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…port different metrics and ignore ones that aren't present
`gauge2!` unconditionally promoted the SVD center matrix to complex
(`complex(c)`), which for a real-valued state forced a complexify then
coerce-back-to-real roundtrip when installing the tensors. Only complexify
when the state's scalartype is itself complex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

SU(2) DMRG performance unexpectedly slower than U(1) ITensor version

3 participants