Repository navigation
More allocator threading - #520
Merged
Merged
Conversation
leburgel
marked this pull request as draft
September 15, 2026 08:58
`TransferMatrix` and its application take `backend`/`allocator` keywords, and the infinite environment routines pass them down from IDMRG and VUMPS. Where sites are spawned, the allocator is derived from the scheduler with `default_allocator`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds `_densify!!`, which takes the dense copy of a block tensor from the allocator rather than the heap. `prepare_operator!!` uses it for `GL_O`/`O_GR`, bracketed by a checkpoint/reset, and passes `copy = true` to the repartitions so the returned environments never share memory the reset releases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`compute_leftenvs!`/`compute_rightenvs!` accepted `backend`/`allocator` only for `InfiniteMPOHamiltonian`, so `recalculate!` raised a `MethodError` for plain `InfiniteMPO`. The `InfiniteMPO` methods now take them and pass them to both `TransferMatrix` constructions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`gaugefix!` takes `backend`/`allocator` as explicit keywords and carries them through `uniform_leftorth!`/`uniform_rightorth!` into the `TransferMatrix` solves and the `repartition!` calls in `gauge_orth_step!`/`regauge!`. `LeftCanonical` and `RightCanonical` gain no fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`leftenv`/`rightenv` take `backend`/`allocator` across the environment types; only `FiniteEnvironments`, which rebuilds lazily, uses them. `hamiltonian_derivatives.jl` passes them at the call sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`QuasiparticleAnsatz` gains a `backend` field with a positional constructor. The `TransferMatrix` constructions in `qp_envs.jl` and the transfer-system solves take `backend`/`allocator`, with the allocator derived from the scheduler. The two transfer systems use `tforeach(1:2; scheduler)` instead of `@sync`/`Threads.@spawn`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
leburgel
force-pushed
the
lb/allocator-threading
branch
from
September 16, 2026 09:08
ee92981 to
6791f59
Compare
leburgel
marked this pull request as ready for review
September 16, 2026 11:45
leburgel
commented
Sep 16, 2026
lkdvos
reviewed
Sep 16, 2026
Member
Author
|
Some benchmark results: Benchmark scriptbenchmark/allocator_benchmark.jl # Allocation benchmark for the backend/allocator threading.
#
# julia --project=benchmark benchmark/allocator_benchmark.jl default
# julia --project=benchmark benchmark/allocator_benchmark.jl buffer
#
# One allocator per process: selecting it is a method redefinition of `default_allocator`, so
# it cannot be varied within a run. Invoke once per allocator and compare the two outputs;
# to measure a change, do the same on both revisions.
#
# Every case is converged first and measured separately, so compilation is not charged to the
# measurement, and the solver tolerance is pinned to zero so each case runs exactly `ITERS`
# iterations -- with a finite tolerance a nearly-converged case exits early while the reported
# figure still divides by `ITERS`, which makes it meaningless.
#
# BLAS is held to one thread so the figure is about allocation rather than threading.
#
# Environment variables: BENCH_ITERS (default 10), BENCH_CHI (default 256),
# BENCH_CYLINDER_CHI (default 128), BENCH_CASES (comma-separated subset of the case names).
using LinearAlgebra, Printf, Random
using TensorOperations: BufferAllocator, DefaultAllocator
using MPSKit, MPSKitModels, TensorKit
BLAS.set_num_threads(1)
const KIND = length(ARGS) >= 1 ? lowercase(ARGS[1]) : "buffer"
KIND in ("default", "buffer") || error("argument must be `default` or `buffer`")
const ITERS = parse(Int, get(ENV, "BENCH_ITERS", "10"))
const CHI = parse(Int, get(ENV, "BENCH_CHI", "256"))
const CYL_CHI = parse(Int, get(ENV, "BENCH_CYLINDER_CHI", "128"))
let alloc = KIND == "default" ? DefaultAllocator() : BufferAllocator()
@eval MPSKit default_allocator(::Type{<:MPSKit.HostStorage}, ::MPSKit.SerialScheduler) = $alloc
end
# Models
# ------
# `tol = 0` everywhere: see the note above.
gs_alg(which, chi, n) = which === :idmrg2 ?
IDMRG2(; trunc = truncrank(chi), maxiter = n, tol = 0.0, verbosity = 0) :
VUMPS(; maxiter = n, tol = 0.0, verbosity = 0)
"Quantum nearest-neighbour Ising, 2-site cell. Dense, no symmetries: the cheap case."
function case_ising_quantum(which)
H = repeat(transverse_field_ising(; g = 1.5), 2)
P = physicalspace(H, 1)
Random.seed!(1234)
psi0 = InfiniteMPS([P, P], [typeof(P)(CHI), typeof(P)(CHI)])
psi, = find_groundstate(psi0, H, gs_alg(which, CHI, 5))
return () -> find_groundstate(psi, H, gs_alg(which, CHI, ITERS), environments(psi, H, psi))
end
"""
Heisenberg S=1 on a width-6 infinite cylinder — the same case MPSKit's own
`benchmark/MPSKitBenchmarks/scripts/setup_heisenberg.jl` uses as its heavy example. Wide MPO,
`BlockTensorMap` environments: this is where the allocator matters.
Run for both symmetries, because they stress different things: `SU2Irrep` gives many small
symmetry blocks, `Trivial` gives one large dense block at the same physical bond dimension.
"""
function case_heisenberg_cylinder(which, symmetry = SU2Irrep)
symmetry === Trivial && return case_heisenberg_cylinder_trivial(which)
H = heisenberg_XXX(Float64, SU2Irrep, InfiniteCylinder(6); spin = 1)
Random.seed!(1234)
# Virtual spaces taken from MPSKit's own cylinder benchmark
# (`benchmark/MPSKitBenchmarks/derivatives/heisenberg_cylinder.toml`). Integer SU(2)
# charges, not half-integer: a six-site spin-1 unit cell fuses to integer total spin, and a
# trivial or half-integer virtual space admits no fusion channels at all -- the initial
# tensors come out rank-zero and the first eigensolve throws `initial vector should not
# have norm zero`.
V = Rep[SU₂](0 => 1, 1 => 1, 2 => 1, 3 => 1)
psi0 = InfiniteMPS(collect(physicalspace(H, i) for i in 1:length(H)), fill(V, length(H)))
# Grow into a nontrivial bond dimension before measuring: a product state measures nothing.
psi, = find_groundstate(psi0, H, gs_alg(:idmrg2, CYL_CHI, 8))
return () -> find_groundstate(psi, H, gs_alg(which, CYL_CHI, ITERS), environments(psi, H, psi))
end
function case_heisenberg_cylinder_trivial(which)
H = heisenberg_XXX(Float64, Trivial, InfiniteCylinder(6); spin = 1)
Random.seed!(1234)
# Same benchmark file, `Trivial` entries: physical ℂ^3, virtual ℂ^3 at the small end.
V = ComplexSpace(8)
psi0 = InfiniteMPS(collect(physicalspace(H, i) for i in 1:length(H)), fill(V, length(H)))
psi, = find_groundstate(psi0, H, gs_alg(:idmrg2, CYL_CHI, 8))
return () -> find_groundstate(psi, H, gs_alg(which, CYL_CHI, ITERS), environments(psi, H, psi))
end
"Classical 2D Ising transfer matrix: a plain `InfiniteMPO` through `leading_boundary`."
function case_ising_classical(which)
O = repeat(classical_ising(; beta = log(1 + sqrt(2)) / 2), 2) # 2-site: IDMRG2 needs >= 2
P = physicalspace(O, 1)
Random.seed!(1234)
psi0 = InfiniteMPS([P, P], [typeof(P)(CHI), typeof(P)(CHI)])
psi, = leading_boundary(psi0, O, gs_alg(which, CHI, 5))
return () -> leading_boundary(psi, O, gs_alg(which, CHI, ITERS), environments(psi, O, psi))
end
"""
Quasiparticle excitations. `excitations` re-applies the effective excitation Hamiltonian on
every Krylov iteration and each application rebuilds the quasiparticle environments, so this
is a per-iteration allocation path. Reported per solve rather than per iteration.
"""
function case_excitations(_)
H = repeat(transverse_field_ising(; g = 1.5), 2)
P = physicalspace(H, 1)
Random.seed!(1234)
psi0 = InfiniteMPS([P, P], [typeof(P)(CHI), typeof(P)(CHI)])
psi, envs = find_groundstate(psi0, H, VUMPS(; maxiter = 25, tol = 1.0e-10, verbosity = 0))
excitations(H, QuasiparticleAnsatz(), 0.0, psi, envs; num = 1) # warm up
return () -> excitations(H, QuasiparticleAnsatz(), 0.0, psi, envs; num = 1)
end
const CASES = [
("ising-quantum idmrg2", () -> case_ising_quantum(:idmrg2), ITERS),
("ising-quantum vumps ", () -> case_ising_quantum(:vumps), ITERS),
("heisenberg-cyl-su2 idmrg2", () -> case_heisenberg_cylinder(:idmrg2, SU2Irrep), ITERS),
("heisenberg-cyl-su2 vumps ", () -> case_heisenberg_cylinder(:vumps, SU2Irrep), ITERS),
("heisenberg-cyl-triv idmrg2", () -> case_heisenberg_cylinder(:idmrg2, Trivial), ITERS),
("heisenberg-cyl-triv vumps ", () -> case_heisenberg_cylinder(:vumps, Trivial), ITERS),
("ising-classical idmrg2", () -> case_ising_classical(:idmrg2), ITERS),
("ising-classical vumps ", () -> case_ising_classical(:vumps), ITERS),
("excitations qp ", () -> case_excitations(nothing), 1),
]
selected = get(ENV, "BENCH_CASES", "")
cases = isempty(selected) ? CASES :
filter(c -> any(occursin(strip(s), first(c)) for s in split(selected, ",")), CASES)
@printf("allocator = %s, iters = %d, chi = %d (cylinder %d), BLAS threads = 1\n\n",
KIND, ITERS, CHI, CYL_CHI)
@printf("%-24s %10s %14s %14s %8s\n", "case", "time [s]", "total [GiB]", "MiB/unit", "GC")
println("-"^76)
for (name, setup, unit) in cases
run = try
setup()
catch e
@printf("%-24s SETUP FAILED: %s\n", name, first(sprint(showerror, e), 80))
continue
end
GC.gc(true)
stats = try
@timed run()
catch e
@printf("%-24s RUN FAILED: %s\n", name, first(sprint(showerror, e), 80))
continue
end
@printf("%-24s %10.2f %14.3f %14.2f %7.1f%%\n", name, stats.time,
stats.bytes / 2^30, stats.bytes / 2^20 / unit, 100 * stats.gctime / stats.time)
endResults (MiB allocated per iteration):
GC time over the same comparison: 11.3 % → 6.7 % (quantum Ising VUMPS), 15.4 % → 8.3 % |
lkdvos
reviewed
Sep 17, 2026
Co-authored-by: Lukas Devos <ldevos98@gmail.com>
lkdvos
approved these changes
Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends the existing backend and allocator handling to thread these through to more subroutines:
I have some bencharks on the way, but from the partial results I've seen this gives quite a big additional reduction in allocations on top of the initial allocator effort.