Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
82242bb
Defaults: add `backend()` and a session-wide buffering switch
lkdvos Aug 3, 2026
285c0e1
Add `default_allocator`: scratch space for local updates
lkdvos Aug 3, 2026
cc94f46
derivatives: backend and allocator on the derivative operators
lkdvos Aug 3, 2026
75c6757
changebonds: configurable backend and allocator
lkdvos Aug 3, 2026
cc50e52
DMRG, IDMRG: configurable backend, reused allocator
lkdvos Aug 3, 2026
ca62b2e
TDVP: configurable, reused backend and allocator
lkdvos Aug 3, 2026
2f9b86a
VUMPS, VOMPS: configurable backend and allocator
lkdvos Aug 3, 2026
d48a95b
GradientGrassmann: configurable backend and allocator
lkdvos Aug 3, 2026
45b16d2
excitations, ED, DDMRG: configurable backend and allocator
lkdvos Aug 3, 2026
cbdacce
docs: the allocator is internal, and device-aware
lkdvos Aug 3, 2026
6aa654f
test: cover the allocator selector, scheduler granularity and GPU alg…
lkdvos Aug 3, 2026
77d2b45
benchmark: sbatch script for the CUDA test suite
lkdvos Aug 3, 2026
6604cd1
BUG: configurable backend, reused allocator
lkdvos Aug 3, 2026
c8078b6
derivatives: route the backend and allocator through the projection a…
lkdvos Aug 3, 2026
9fb230c
calc_galerkin: accept a backend and allocator
lkdvos Aug 3, 2026
024cb05
IDMRG: obtain the allocator once per solve
lkdvos Aug 3, 2026
0ae0b39
derivatives: restrict the Jordan operators' backend type parameter
lkdvos Aug 3, 2026
65efc79
docs: changelog entry for the backend setting and the allocator
lkdvos Aug 3, 2026
f7212db
test: fix the GPU setup's imports, and cover BUG on device
lkdvos Aug 3, 2026
f587f79
changebonds: hand the expansion the sweep's allocator
lkdvos Aug 3, 2026
453319b
derivatives: `prepare_operator!!` reads the backend and allocator off…
lkdvos Aug 4, 2026
650beae
test: fold the allocator coverage into the existing suites
lkdvos Aug 4, 2026
6d01ed2
Trim allocator comments, and drop the GPU benchmark script
lkdvos Aug 4, 2026
0c9ec86
test: do not shadow the starting state in the scheduler-parametrized …
lkdvos Aug 4, 2026
1d34dd9
test: build the scheduler-parametrized starting states from scratch
lkdvos Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36"
MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4"
OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5"
OptimKit = "77e91f04-9b3b-57a6-a776-40b61faaebe0"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
Expand Down Expand Up @@ -46,12 +47,13 @@ LoggingExtras = "~1.0"
MatrixAlgebraKit = "0.6.5"
OhMyThreads = "0.7, 0.8"
OptimKit = "0.3.1, 0.4, 0.5"
Preferences = "1"
Printf = "1"
Random = "1"
RecipesBase = "1.1"
TensorKit = "0.17.1"
TensorKitManifolds = "0.7, 0.8"
TensorOperations = "5.5.1"
TensorOperations = "5.6.2"
TimerOutputs = "0.5.29"
VectorInterface = "0.2, 0.3, 0.4, 0.5, 0.6"
julia = "1.10"
6 changes: 6 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ When releasing a new version, move the "Unreleased" changes to a new version sec
Unlike `TDVP` it has no backward-in-time substep (stable for imaginary-time evolution),
and passing a truncating `trunc` enables rank-adaptivity (the bond dimension grows and shrinks
automatically to track entanglement).
- A `backend` setting on every algorithm, for its tensor contractions and index manipulations,
defaulting to `MPSKit.Defaults.backend()`. ([#467](https://github.com/QuantumKitHub/MPSKit.jl/pull/467))
- Local updates now serve their intermediate tensors from a dedicated allocator, selected internally
by `MPSKit.default_allocator`, instead of leaving them to the garbage collector
(two-site DMRG: -64% allocations, -57% GC time, -23% wall time).
Disable with `MPSKit.Defaults.set_buffering!(false)`. ([#467](https://github.com/QuantumKitHub/MPSKit.jl/pull/467))

### Changed

Expand Down
23 changes: 23 additions & 0 deletions docs/src/man/parallelism.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,29 @@ MPSKit.Defaults.set_scheduler!(:dynamic) # default: multithreading with some loa
For further reference on the available schedulers and finer control, please refer to the
[`OhMyThreads.jl` documentation](https://juliafolds2.github.io/OhMyThreads.jl/stable/)

## Allocators

Every local update in an MPS algorithm contracts a handful of intermediate tensors that are thrown away immediately afterwards.
Rather than asking Julia's garbage collector to clean up after each one, MPSKit hands the contractions an allocator that manages those intermediates itself.
On a typical ground-state search this cuts both the allocation count and the garbage-collection time substantially.

Which allocator is appropriate depends on two things, so [`MPSKit.default_allocator`](@ref) is asked for one at the start of every solve, and the answer is then used for all of its local updates:

- *Where the tensors live.* Host memory can be served by an allocator that bypasses Julia's memory manager, whereas a device-backed state needs its scratch space on the device. This is only known once there is a state in hand, which is why it is not a setting on the algorithm.
- *Whether the allocator will be shared between tasks.* `TensorOperations.BufferAllocator` is a bump buffer with a mutable offset, so it is only safe when a single task owns it - a serial sweep. Concurrent work instead gets a `TensorOperations.ManualAllocator`, which `malloc`s and `free`s each intermediate and holds no state at all.

Anything MPSKit does not recognise falls back on `TensorOperations.DefaultAllocator`, which allocates through the storage type itself: correct on any device, at the cost of leaving the intermediates to the garbage collector.
That is also what device-backed states get.
Note that the `backend` field needs no such treatment: `TensorOperations.DefaultBackend` is a placeholder that resolves to an implementation based on the types of the tensors involved, so it already selects cuTENSOR for `CuArray`-backed states.

Dedicated scratch space can be turned off when memory rather than time is the binding constraint, which trades it back for garbage-collector pressure:

```julia
MPSKit.Defaults.set_buffering!(false)
```

This is a compile-time preference: it is written to a `LocalPreferences.toml` file next to the active `Project.toml`, and Julia has to be restarted for the change to take effect.

## TensorKit multithreading

Finally, when dealing with tensors that have some internal symmetry, it is also possible to
Expand Down
4 changes: 3 additions & 1 deletion src/MPSKit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ using Compat: @compat
@compat public DynamicTols, DynamicTol, AdaptiveKrylov
@compat public VERBOSE_NONE, VERBOSE_WARN, VERBOSE_CONV, VERBOSE_ITER, VERBOSE_ALL
@compat public IterLog, loginit!, logiter!, logfinish!, logcancel!
@compat public default_allocator

# Imports
# -------
Expand All @@ -69,7 +70,7 @@ using BlockTensorKit
using BlockTensorKit: TensorMapSumSpace
using TensorOperations
using TensorOperations: AbstractBackend, DefaultBackend, DefaultAllocator, BufferAllocator,
allocator_checkpoint!, allocator_reset!
ManualAllocator, allocator_checkpoint!, allocator_reset!
using KrylovKit
using KrylovKit: KrylovAlgorithm
using OptimKit
Expand Down Expand Up @@ -99,6 +100,7 @@ using .DynamicTols

include("utility/defaults.jl")
using .Defaults: VERBOSE_NONE, VERBOSE_WARN, VERBOSE_CONV, VERBOSE_ITER, VERBOSE_ALL
include("utility/allocator.jl")
include("utility/logging.jl")
using .IterativeLoggers
include("utility/iterativesolvers.jl")
Expand Down
10 changes: 7 additions & 3 deletions src/algorithms/ED.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
exact_diagonalization(
H::FiniteMPOHamiltonian;
sector = rightunit(H), num::Int = 1, which::Symbol = :SR,
alg = Defaults.alg_eigsolve(; dynamic_tols = false)
alg = Defaults.alg_eigsolve(; dynamic_tols = false),
backend = Defaults.backend()
) -> vals, state_vecs, convhist

Use [`KrylovKit.eigsolve`](@extref) to perform exact diagonalization on a
Expand All @@ -21,6 +22,7 @@ equivalent to dense eigenvectors.
- `which::Symbol = :SR`: the kind eigenvalues to find, see [`KrylovKit.eigsolve`](@extref).
- `alg = Defaults.alg_eigsolve(; dynamic_tols = false)`: the diagonalization algorithm to use,
see [`KrylovKit.eigsolve`](@extref).
- `backend = Defaults.backend()`: backend for tensor contractions and index manipulations.

!!! note "Valid `sector` values"
The total charge of the eigenvectors is imposed by adding a charged auxiliary space as
Expand All @@ -34,7 +36,8 @@ equivalent to dense eigenvectors.
function exact_diagonalization(
H::FiniteMPOHamiltonian;
sector = rightunit(H), num::Int = 1, which::Symbol = :SR,
alg = Defaults.alg_eigsolve(; dynamic_tols = false)
alg = Defaults.alg_eigsolve(; dynamic_tols = false),
backend::AbstractBackend = Defaults.backend()
)
L = length(H)
@assert L > 1 "FiniteMPOHamiltonian must have length > 1"
Expand Down Expand Up @@ -74,7 +77,8 @@ function exact_diagonalization(
# optimize the middle site
# Because the MPS is full rank - this is equivalent to the full Hamiltonian
AC₀ = state.AC[middle_site]
H_ac = AC_hamiltonian(middle_site, state, H, state, envs)
allocator = default_allocator(state, SerialScheduler())
H_ac = AC_hamiltonian(middle_site, state, H, state, envs; backend, allocator)
vals, vecs, convhist = eigsolve(H_ac, AC₀, num, which, alg)

# repack eigenstates
Expand Down
6 changes: 4 additions & 2 deletions src/algorithms/approximate/fvomps.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
function approximate!(ψ::AbstractFiniteMPS, Oϕ, alg::DMRG2, envs = environments(ψ, _environment_args(Oϕ)...))
allocator = default_allocator(ψ, SerialScheduler())
ϵ::Float64 = 2 * alg.tol
log = IterLog("DMRG2")

Expand All @@ -7,7 +8,7 @@ function approximate!(ψ::AbstractFiniteMPS, Oϕ, alg::DMRG2, envs = environment
for iter in 1:(alg.maxiter)
ϵ = 0.0
for pos in [1:(length(ψ) - 1); (length(ψ) - 2):-1:1]
AC2′ = AC2_projection(pos, ψ, Oϕ, envs)
AC2′ = AC2_projection(pos, ψ, Oϕ, envs; alg.backend, allocator)
al, c, ar, = svd_trunc!(AC2′, inner_alg_gauge(alg))

AC2 = ψ.AC[pos] * _transpose_tail(ψ.AR[pos + 1])
Expand Down Expand Up @@ -36,6 +37,7 @@ function approximate!(ψ::AbstractFiniteMPS, Oϕ, alg::DMRG2, envs = environment
end

function approximate!(ψ::AbstractFiniteMPS, Oϕ, alg::DMRG, envs = environments(ψ, _environment_args(Oϕ)...))
allocator = default_allocator(ψ, SerialScheduler())
ϵ::Float64 = 2 * alg.tol
log = IterLog("DMRG")

Expand All @@ -44,7 +46,7 @@ function approximate!(ψ::AbstractFiniteMPS, Oϕ, alg::DMRG, envs = environments
for iter in 1:(alg.maxiter)
ϵ = 0.0
for pos in [1:(length(ψ) - 1); length(ψ):-1:2]
AC′ = AC_projection(pos, ψ, Oϕ, envs)
AC′ = AC_projection(pos, ψ, Oϕ, envs; alg.backend, allocator)
AC = ψ.AC[pos]
ϵ = max(ϵ, norm(AC′ - AC) / norm(AC′))

Expand Down
23 changes: 16 additions & 7 deletions src/algorithms/approximate/idmrg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ function approximate!(
ψ::MultilineMPS, toapprox::Tuple{<:MultilineMPO, <:MultilineMPS}, alg::IDMRG,
envs = environments(ψ, toapprox...)
)
allocator = default_allocator(ψ, SerialScheduler())
log = IterLog("IDMRG")
ϵ::Float64 = 2 * alg.tol
local iter
Expand All @@ -15,7 +16,8 @@ function approximate!(
for col in 1:size(ψ, 2)
for row in 1:size(ψ, 1)
ψ.AC[row + 1, col] = AC_projection(
CartesianIndex(row, col), ψ, toapprox, envs
CartesianIndex(row, col), ψ, toapprox, envs;
alg.backend, allocator
)
normalize!(ψ.AC[row + 1, col])
ψ.AL[row + 1, col], ψ.C[row + 1, col] = left_orth!(ψ.AC[row + 1, col])
Expand All @@ -27,7 +29,8 @@ function approximate!(
for col in reverse(1:size(ψ, 2))
for row in 1:size(ψ, 1)
ψ.AC[row + 1, col] = AC_projection(
CartesianIndex(row, col), ψ, toapprox, envs
CartesianIndex(row, col), ψ, toapprox, envs;
alg.backend, allocator
)
normalize!(ψ.AC[row + 1, col])
ψ.C[row + 1, col - 1], temp = right_orth!(_transpose_tail(ψ.AC[row + 1, col]))
Expand Down Expand Up @@ -64,6 +67,7 @@ function approximate!(
ψ::MultilineMPS, toapprox::Tuple{<:MultilineMPO, <:MultilineMPS},
alg::IDMRG2, envs = environments(ψ, toapprox...)
)
allocator = default_allocator(ψ, SerialScheduler())
size(ψ, 2) < 2 && throw(ArgumentError("unit cell should be >= 2"))
ϵ::Float64 = 2 * alg.tol
log = IterLog("IDMRG2")
Expand All @@ -80,7 +84,7 @@ function approximate!(
for row in 1:size(ψ, 1)
AC2′ = AC2_projection(
CartesianIndex(row, site), ψ, toapprox, envs;
kind = :ACAR
kind = :ACAR, alg.backend, allocator
)
al, c, ar = svd_trunc!(AC2′; trunc = alg.trunc, alg = alg.alg_svd)
normalize!(c)
Expand All @@ -99,7 +103,10 @@ function approximate!(
ψ.AL[1, end] = ψ.AC[1, end] / ψ.C[1, end]
ψ.AC[1, 1] = _mul_tail(ψ.AL[1, 1], ψ.C[1, 1])
for row in 1:size(ψ, 1)
AC2′ = AC2_projection(CartesianIndex(row, size(ψ, 2)), ψ, toapprox, envs; kind = :ALAC)
AC2′ = AC2_projection(
CartesianIndex(row, size(ψ, 2)), ψ, toapprox, envs;
kind = :ALAC, alg.backend, allocator
)
al, c, ar = svd_trunc!(AC2′; trunc = alg.trunc, alg = alg.alg_svd)
normalize!(c)

Expand All @@ -123,7 +130,7 @@ function approximate!(
for row in 1:size(ψ, 1)
AC2′ = AC2_projection(
CartesianIndex(row, site), ψ, toapprox, envs;
kind = :ALAC
kind = :ALAC, alg.backend, allocator
)
al, c, ar = svd_trunc!(AC2′; trunc = alg.trunc, alg = alg.alg_svd)
normalize!(c)
Expand All @@ -141,7 +148,10 @@ function approximate!(
ψ.AC[1, end] = _mul_front(ψ.C[1, end - 1], ψ.AR[1, end])
ψ.AR[1, 1] = _transpose_front(ψ.C[1, end] \ _transpose_tail(ψ.AC[1, 1]))
for row in 1:size(ψ, 1)
AC2′ = AC2_projection(CartesianIndex(row, 0), ψ, toapprox, envs; kind = :ACAR)
AC2′ = AC2_projection(
CartesianIndex(row, 0), ψ, toapprox, envs;
kind = :ACAR, alg.backend, allocator
)
al, c, ar = svd_trunc!(AC2′; trunc = alg.trunc, alg = alg.alg_svd)
normalize!(c)

Expand All @@ -155,7 +165,6 @@ function approximate!(
transfer_leftenv!(envs, ψ, toapprox, 1)
transfer_rightenv!(envs, ψ, toapprox, 0)


normalize!(envs, ψ, toapprox)

# update error
Expand Down
41 changes: 32 additions & 9 deletions src/algorithms/approximate/vomps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ Base.@deprecate(
),
approximate(
ψ, toapprox,
VOMPS(; alg.tol, alg.maxiter, alg.finalize, alg.verbosity, alg.alg_gauge, alg.alg_environments),
VOMPS(;
alg.tol, alg.maxiter, alg.finalize, alg.verbosity, alg.alg_gauge,
alg.alg_environments, alg.backend,
),
envs...; kwargs...
)
)
Expand All @@ -14,9 +17,13 @@ function approximate(
mps::MultilineMPS, toapprox::Tuple{<:MultilineMPO, <:MultilineMPS}, alg::VOMPS,
envs = environments(mps, toapprox...)
)
return _approximate_vomps(mps, toapprox, alg, envs)
end

function _approximate_vomps(mps, toapprox, alg::VOMPS, envs)
log = IterLog("VOMPS")
iter = 0
ϵ = calc_galerkin(mps, toapprox..., envs)
ϵ = calc_galerkin(mps, toapprox..., envs; alg.backend)
alg_environments = adapt_solver(alg.alg_environments; iter, g_global = ϵ)
recalculate!(envs, mps, toapprox..., alg_environments)

Expand Down Expand Up @@ -54,7 +61,7 @@ function Base.iterate(it::IterativeSolver{<:VOMPS}, state::VOMPSState{<:Any, <:T
mps, envs = it.finalize(state.iter, mps, state.operator, envs)::typeof((mps, envs))

# error criterion
ϵ = calc_galerkin(mps, state.operator..., envs)
ϵ = calc_galerkin(mps, state.operator..., envs; it.backend)

# update state
it.state = VOMPSState(mps, state.operator, envs, state.iter + 1, ϵ)
Expand All @@ -71,17 +78,24 @@ function localupdate_step!(
ACs = similar(state.mps.AC)
dst_ACs = state.mps isa Multiline ? eachcol(ACs) : ACs

foreach(eachsite(state.mps)) do site
# the sweep is serial, so a single allocator serves all sites
allocator = default_allocator(state.mps, SerialScheduler())
for site in eachsite(state.mps)
AC = map(1:size(state.mps, 1)) do row
AC_projection(CartesianIndex(row, site), state.mps, state.operator, state.envs)
AC_projection(
CartesianIndex(row, site), state.mps, state.operator, state.envs;
it.backend, allocator
)
end
circshift!(AC, 1)
C = map(1:size(state.mps, 1)) do row
C_projection(CartesianIndex(row, site), state.mps, state.operator, state.envs)
C_projection(
CartesianIndex(row, site), state.mps, state.operator, state.envs;
it.backend, allocator
)
end
circshift!(C, 1)
dst_ACs[site] = regauge!(AC, C; alg = alg_orth)
return nothing
end

return ACs
Expand All @@ -95,18 +109,27 @@ function localupdate_step!(
ACs = similar(state.mps.AC)
dst_ACs = state.mps isa Multiline ? eachcol(ACs) : ACs

# every site - and the AC and C projections within a site - runs concurrently, so the allocator
# is shared and has to be one that tolerates that
allocator = default_allocator(state.mps, scheduler)
tforeach(eachsite(state.mps); scheduler) do site
local AC, C
@sync begin
Threads.@spawn begin
AC = map(1:size(state.mps, 1)) do row
AC_projection(CartesianIndex(row, site), state.mps, state.operator, state.envs)
AC_projection(
CartesianIndex(row, site), state.mps, state.operator, state.envs;
it.backend, allocator
)
end
circshift!(AC, 1)
end
Threads.@spawn begin
C = map(1:size(state.mps, 1)) do row
C_projection(CartesianIndex(row, site), state.mps, state.operator, state.envs)
C_projection(
CartesianIndex(row, site), state.mps, state.operator, state.envs;
it.backend, allocator
)
end
circshift!(C, 1)
end
Expand Down
Loading
Loading