Skip to content

Construct fusion tree blocks from the shorter side, and group the cached trees in fusionblocks - #581

Merged
lkdvos merged 12 commits into
mainfrom
lb/fusiontreeblock_construction
Oct 28, 2026
Merged

lkdvos merged 12 commits into
mainfrom
lb/fusiontreeblock_construction

Conversation

@leburgel

Copy link
Copy Markdown
Member

This is a draft illustrating some potential performance improvements; the actual implementations are
very much up for discussion.

Same blocks, same order, less work:

  • FusionTreeBlock{I}(uncoupled, isdual): for SimpleUnit sectors, the coupled sectors come from
    the fusion product of the shorter side only, instead of intersecting the products of both sides.
  • fusionblocks(W): groups the cached fusiontrees(W) by uncoupled sectors, instead of constructing
    a (mostly empty) block for every combination of sectors.

New test: the blocks of a HomSpace hold exactly its trees, in order.

Cold construction of the recoupling data for one permutation of a tensor from a PEPS CTMRG step
(scripts below), main → this PR:

main this PR
fℤ₂ × U(1) × SU(3), 1 thread 5.29 s 4.96 s (1.07×)
fℤ₂ × U(1) × SU(3), 8 threads 1.58 s 1.42 s (1.11×)
fℤ₂ × U(1) × SU(2) × U(1), 1 thread 4.99 s 3.20 s (1.6×)
fℤ₂ × U(1) × SU(2) × U(1), 8 threads 3.45 s 2.40 s (1.4×)

SU(2) gains little from threads: Fsymbol for SU2Irrep goes through WignerSymbols, which takes
a lock on every call. Removing that lock (caching the F-symbols in TensorKitSectors) is scheduled
as a follow-up.

TensorKit main (ae8abfc), SUNRepresentations c02a3f3, TensorKitSectors v0.3.10, Julia 1.12.5; shared 80-core
machine, two interleaved rounds agree within 15 %.

fℤ₂ × U(1) × SU(2) × U(1) reproducer (julia -t8)
using TensorKit

# the spaces of one index permutation in a CTMRG step for a fℤ₂ × U(1) × SU(2) × U(1)-symmetric PEPS
const I = FermionParity ⊠ U1Irrep ⊠ SU2Irrep ⊠ U1Irrep
s(f, q, j, q′) = FermionParity(f) ⊠ U1Irrep(q) ⊠ SU2Irrep(j) ⊠ U1Irrep(q′)
V1 = Vect[I](s(0, 0, 1/2, 1) => 1, s(1, -1, 0, 0) => 1, s(0, 2, 0, 0) => 1, s(0, 0, 0, -2) => 1, s(1, 1, 1/2, -1) => 1, s(1, 1, 0, 2) => 1)
V2 = Vect[I](s(0, 0, 1/2, 1) => 1, s(0, 2, 0, 0) => 1, s(0, 0, 0, -2) => 1, s(1, 1, 1/2, -1) => 1, s(1, 1, 0, 2) => 1)
V3 = Vect[I](s(1, 1, 0, 0) => 1, s(0, 0, 1/2, -1) => 1, s(0, 0, 0, 2) => 1, s(0, 2, 1/2, 1) => 1, s(0, 2, 0, -2) => 1)
V4 = Vect[I](s(1, 1, 0, 0) => 1, s(0, 0, 1/2, -1) => 1, s(0, 0, 0, 2) => 1, s(1, -1, 1/2, 1) => 1, s(1, -1, 0, -2) => 1)
V5 = Vect[I](s(0, 0, 0, 0) => 5, s(0, 0, 1, 0) => 1, s(1, 1, 1/2, 1) => 4, s(1, -1, 1/2, -1) => 4, s(0, 0, 1/2, 3) => 1, s(1, 1, 0, -2) => 3, s(1, -1, 0, 2) => 3, s(0, 0, 1/2, -3) => 1, s(1, 1, 1, -2) => 1, s(1, -1, 1, 2) => 1, s(1, 1, 0, 4) => 1, s(1, -1, 0, -4) => 1)
V6 = Vect[I](s(0, 0, 0, 0) => 5, s(0, 0, 1, 0) => 2, s(1, 1, 1/2, 1) => 4, s(1, -1, 1/2, -1) => 3, s(0, 0, 1/2, 3) => 2, s(0, 2, 1/2, -1) => 1, s(0, -2, 1/2, 1) => 1, s(1, 1, 0, -2) => 3, s(1, -1, 0, 2) => 3, s(0, 0, 1/2, -3) => 2, s(0, 2, 0, 2) => 1, s(1, 1, 1, -2) => 1, s(0, -2, 0, -2) => 1, s(1, 1, 0, 4) => 1)
V7 = Vect[I](s(0, 0, 1/2, 1) => 1, s(1, -1, 0, 0) => 1, s(0, 0, 0, -2) => 1, s(1, 1, 1/2, -1) => 1, s(1, 1, 0, 2) => 1)
W = (V1 ⊗ V2' ⊗ V3) ← (V4' ⊗ V5 ⊗ V6' ⊗ V7)
p = ((1, 7, 4), (2, 3, 5, 6))

t = randn(W)

# cold construction of the cached recoupling data of the permutation (TensorKit's caches emptied,
# the symbols warm), with 1 and with all threads
function cold_permute(t, p, n)
    TensorKit.set_num_manipulation_threads(n)
    TensorKit.set_num_transformer_threads(n)
    TensorKit.empty_globalcaches!()
    return @elapsed permute(t, p)
end
cold_permute(t, p, Threads.nthreads()) # compilation, and the symbols
for n in unique((1, Threads.nthreads()))
    ts = [cold_permute(t, p, n) for _ in 1:3]
    println("cold permute, $n thread(s): ", round(minimum(ts); digits = 2), " s (min of 3)")
end
fℤ₂ × U(1) × SU(3) reproducer (julia -t8)
using SUNRepresentations, TensorKit
SUNRepresentations.use_disk_cache(false; persist = false)

# the spaces of one index permutation in a CTMRG step for a fℤ₂ × U(1) × SU(3)-symmetric PEPS
const I = FermionParity ⊠ U1Irrep ⊠ SU3Irrep
s(f, q, a, b) = FermionParity(f) ⊠ U1Irrep(q) ⊠ SU3Irrep(a, b)
V1 = Vect[I](s(0, 0, 1, 0) => 1, s(1, 1, 0, 1) => 1, s(1, -1, 0, 0) => 1, s(0, 2, 0, 0) => 1)
V2 = Vect[I](s(0, 0, 1, 0) => 1, s(1, -1, 0, 0) => 1, s(0, -2, 0, 1) => 1)
V3 = Vect[I](s(0, 0, 1, 0) => 1, s(1, 1, 0, 1) => 1, s(1, -1, 0, 0) => 1)
V4 = Vect[I](s(0, 0, 0, 0) => 1, s(1, -1, 0, 1) => 1, s(0, -2, 1, 0) => 1)
V5 = Vect[I](s(0, 0, 0, 0) => 2, s(0, 0, 1, 1) => 2, s(1, 1, 1, 0) => 2, s(1, -1, 0, 1) => 2, s(1, 1, 0, 2) => 1, s(0, 2, 0, 1) => 1, s(0, -2, 1, 0) => 1, s(1, -1, 2, 0) => 1)
V6 = Vect[I](s(0, 0, 0, 0) => 3, s(0, 0, 1, 1) => 2, s(1, 1, 1, 0) => 2, s(1, -1, 0, 1) => 2, s(0, 2, 0, 1) => 1, s(0, -2, 1, 0) => 1, s(0, -2, 0, 2) => 1, s(0, 2, 2, 0) => 1)
V7 = Vect[I](s(0, 0, 0, 0) => 1, s(1, 1, 1, 0) => 1, s(1, -1, 0, 1) => 1)
W = (V1 ⊗ V2 ⊗ V3) ← (V4 ⊗ V5 ⊗ V6' ⊗ V7)
p = ((1, 7, 4), (2, 3, 5, 6))

t = randn(W)

# cold construction of the cached recoupling data of the permutation (TensorKit's caches emptied,
# the F- and R-symbols warm), with 1 and with all threads
function cold_permute(t, p, n)
    TensorKit.set_num_manipulation_threads(n)
    TensorKit.set_num_transformer_threads(n)
    TensorKit.empty_globalcaches!()
    return @elapsed permute(t, p)
end
cold_permute(t, p, Threads.nthreads()) # compilation, and the symbols
for n in unique((1, Threads.nthreads()))
    ts = [cold_permute(t, p, n) for _ in 1:3]
    println("cold permute, $n thread(s): ", round(minimum(ts); digits = 2), " s (min of 3)")
end

🤖 Generated with Claude Code

Comment thread src/fusiontrees/fusiontrees.jl Outdated
Comment thread src/fusiontrees/fusiontrees.jl Outdated
Comment thread src/spaces/homspace.jl Outdated
Comment thread src/spaces/homspace.jl
@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/fusiontrees/fusiontrees.jl 94.21% <100.00%> (-2.46%) ⬇️
src/spaces/homspace.jl 93.63% <100.00%> (+0.16%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/spaces/homspace.jl Outdated
@leburgel

Copy link
Copy Markdown
Member Author

Done: the blocks now come in the order in which their uncoupled sectors first occur in fusiontrees(W), without the sort. The order test is replaced by a check that the blocks are unique.

The sortperm wasn't 1:N: over one CTMRG iteration, 381 of 554 (fℤ₂ × U(1) × SU(3)) and 413 of 550 (fℤ₂ × U(1) × SU(2) × U(1)) transformers now get their blocks in a different order. The transformers are otherwise identical.

fusionblocks over all spaces of one CTMRG iteration (shared machine, load 20–25, so not comparable to the numbers above):

sortperm first occurrence
fℤ₂ × U(1) × SU(3) 1.17–1.23 s 0.86 s
fℤ₂ × U(1) × SU(2) × U(1) 9.3–9.8 s 5.25 s

@lkdvos lkdvos 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.

For me this looks good to go actually. Thanks for this!

Comment thread src/fusiontrees/fusiontrees.jl Outdated
Comment thread src/spaces/homspace.jl Outdated
leburgel and others added 12 commits October 28, 2026 15:19
For sectors with a simple unit, a superset of the coupled sectors suffices, as the fusion tree iterators yield nothing for the others: the unit if one side is empty, and otherwise the fusion product of the shorter side only, instead of the intersection of the fusion products of both sides. The trees and their order are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Instead of constructing a block for every combination of codomain and domain sectors, `fusionblocks` groups the cached `fusiontrees(W)` by their uncoupled sectors, in the same order as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`sort!(...; by = rank)` evaluated `rank`, and with it the `Dict` lookups of (product) sectors, at every comparison.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… only the longer side for trees

Co-Authored-By: Claude Opus 5.5 <[email protected]>
For several units, a coupled sector of a block must be compatible with the units at both ends of either side; otherwise the fusion tree iterator throws instead of being empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@lkdvos
lkdvos force-pushed the lb/fusiontreeblock_construction branch from 8e373fa to 3f74b72 Compare October 28, 2026 19:19
@lkdvos
lkdvos enabled auto-merge (squash) October 28, 2026 19:23
@lkdvos
lkdvos disabled auto-merge October 28, 2026 22:53
@lkdvos
lkdvos merged commit 8f7af24 into main Oct 28, 2026
46 of 47 checks passed
@lkdvos
lkdvos deleted the lb/fusiontreeblock_construction branch October 28, 2026 22:53
@leburgel leburgel mentioned this pull request Oct 29, 2026
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.

3 participants