Skip to content

perf: fix performance regression for non-abelian index manipulations - #521

Merged
lkdvos merged 4 commits into
mainfrom
ld-indexmanipulation
Aug 31, 2026
Merged

perf: fix performance regression for non-abelian index manipulations#521
lkdvos merged 4 commits into
mainfrom
ld-indexmanipulation

Conversation

@lkdvos

@lkdvos lkdvos commented Aug 30, 2026

Copy link
Copy Markdown
Member

This refactors some of the process for handling the allocators for the implementation of the GenericTreeTransformer, which should restore the performance from v0.16.5 by no longer having to allocate buffers for each separate block. Along the way I also added taskforeach since that pattern kept showing up and the version with resources is hard to express with OhMyThreads.jl, so in some follow-up PRs I might actually switch more things to that as well.

This fixes #517.

@lkdvos

lkdvos commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

Some benchmark results to show that this resolves #517, using the reproduction script from that issue (single-threaded, Julia 1.12.6; different machine than the issue's numbers, so compare ratios rather than absolute times). Minimum times, allocations per call in parentheses:

permute(t, p):

sector type v0.16.5 v0.17.1 (main) this PR PR / main PR / v0.16.5
fℤ₂ 0.0915 ms (65) 0.0835 ms (63) 0.0555 ms (73) 0.66× 0.61×
fℤ₂ ⊠ U(1) 0.0327 ms (317) 0.0423 ms (315) 0.0374 ms (325) 0.88× 1.15×
SU(2) 0.1558 ms (1841) 0.3155 ms (2845) 0.1955 ms (1932) 0.62× 1.25×
fℤ₂ ⊠ U(1) ⊠ SU(3) 0.0253 ms (470) 0.0427 ms (649) 0.0263 ms (491) 0.62× 1.04×

braid(t, p, levels):

sector type v0.16.5 v0.17.1 (main) this PR PR / main PR / v0.16.5
fℤ₂ 0.0933 ms 0.0627 ms 0.0552 ms 0.88× 0.59×
fℤ₂ ⊠ U(1) 0.0312 ms 0.0424 ms 0.0374 ms 0.88× 1.20×
SU(2) 0.1570 ms 0.3163 ms 0.1793 ms 0.57× 1.14×
fℤ₂ ⊠ U(1) ⊠ SU(3) 0.0255 ms 0.0427 ms 0.0267 ms 0.63× 1.05×

The 1.7–1.9× regression relative to v0.16.5 that the issue reported is gone for the non-abelian cases, and the allocation counts are back at v0.16.5 levels (SU(2): 2845 → 1932 vs 1841 on v0.16.5).

A few additional observations:

  • The issue's tensors are quite small. On larger tensors (4-leg permutes of SU2Space(0 => 12, 1/2 => 10, 1 => 6, 3/2 => 4)), this branch measures at parity with v0.16.5 single-threaded (0.80 ms vs 0.84 ms median) and ahead of it with set_num_transformer_threads(4) (0.38 ms vs 0.44 ms median), since the calling thread now participates as a worker.
  • For the very small non-abelian tensors of the issue there is a small residual gap vs v0.16.5 (SU(2) ~1.2×) that is per-call overhead rather than the buffer strategy; this will be investigated in a follow-up (e.g. normalizing levels in the treebraider cache key for symmetric braidings).

@lkdvos
lkdvos marked this pull request as ready for review August 30, 2026 09:16
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.28571% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/auxiliary/auxiliary.jl 84.37% 5 Missing ⚠️
src/tensors/treetransformers.jl 94.44% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/tensors/indexmanipulations.jl 86.45% <100.00%> (-1.90%) ⬇️
src/tensors/treetransformers.jl 95.78% <94.44%> (+13.26%) ⬆️
src/auxiliary/auxiliary.jl 90.80% <84.37%> (-3.75%) ⬇️

... and 7 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 commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

Test failure is unrelated

@lkdvos
lkdvos requested review from Jutho and leburgel August 30, 2026 13:26
@lkdvos

lkdvos commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

Follow-up with the full benchmark suite, run via AirspeedVelocity on a dedicated cluster node (exclusive node, Julia 1.12.6, --exeflags="--threads=4"; note this leaves the transformer threading at its default of 1, so these numbers reflect the serial kernel). All three revisions ran this branch's benchmark script, which adds the SU2Irrep permute cases to the suite.

indexmanipulations group (mean ± std):

benchmark (T, I, dims, sigmas, p) this PR main v0.16.5 PR / main PR / v0.16.5
C64, SU₂, 48⁴, [[1,3],[2,4]] 1.18 ± 0.21 ms 1.45 ± 0.4 ms 1.01 ± 0.072 ms 0.81× 1.17×
C64, SU₂, 48⁴, [[4,2,3],[1]] 1.21 ± 0.11 ms 1.34 ± 0.22 ms 0.983 ± 0.038 ms 0.90× 1.23×
F64, SU₂, 48⁴, [[1,3],[2,4]] 0.713 ± 0.055 ms 0.843 ± 0.15 ms 0.609 ± 0.026 ms 0.85× 1.17×
F64, SU₂, 48⁴, [[4,2,3],[1]] 0.702 ± 0.12 ms 0.874 ± 0.1 ms 0.643 ± 0.028 ms 0.80× 1.09×
F64, SU₂, 512², [[2,1],[]] 0.087 ± 0.007 ms 0.155 ± 0.021 ms 0.098 ± 0.006 ms 0.56× 0.89×
F64, Trivial, [43408,1216] 54.0 ± 9.1 ms 54.4 ± 0.6 ms 45.0 ± 0.4 ms 0.99× 1.20×
F64, Trivial, [7264,7264] 45.7 ± 0.7 ms 56.2 ± 0.8 ms 45.9 ± 0.4 ms 0.81× 1.00×
F64, Z₂, [43408,1216] 27.3 ± 2.7 ms 28.7 ± 0.5 ms 23.7 ± 4.2 ms 0.95× 1.15×
F64, Z₂, [7264,7264] 23.9 ± 4.6 ms 23.7 ± 4.7 ms 23.0 ± 0.3 ms 1.01× 1.04×

Rest of the suite (127 linalg / tensor-network benchmarks): median PR/main = 0.99 and median PR/v0.16.5 = 0.99, i.e. no systematic change outside the targeted kernels. The tails:

  • The largest PR-vs-main swings in either direction (1.56× and 0.60×) occur on microsecond-scale mpo cases — including a Trivial one that never touches the changed kernels — and mirror each other, so these are benchmark noise rather than signal.
  • The largest PR-vs-v0.16.5 ratios (~1.45–1.56×) are all small-block SU₂ contractions (mera/pepo with sigma = 2.0), where main itself sits at ~2.1–2.3× vs v0.16.5. This PR recovers a good part of that (PR/main ≈ 0.68) but not all: with very small multiplicities the remaining per-call overhead noted above dominates. Same story as the SU₂ 48⁴ rows (suite spaces have much smaller blocks than the tensors I benchmarked manually, where the PR reaches parity with v0.16.5), and the planned follow-up on the per-call/cache overhead should address it.

Comment thread src/auxiliary/auxiliary.jl Outdated
Comment on lines +124 to +128
for j in 2:min(length(resources), n)
local r = resources[j]
Threads.@spawn _taskforeach_worker(Base.Fix2(f, r), items′, counter, n)
end
_taskforeach_worker(Base.Fix2(f, first(resources)), items′, counter, n)

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.

Does this need an inbounds for the resources indexing ? Probably that is negligible overhead compared to spawning tasks?

I would also write resources[1] instead of first(resources) for consistency with the other indexing, but that is a personal matter of taste.

Use a let block for the per-task resource capture, make the two methods
structurally identical (shared nworkers local and zero-worker early
return), and update the docstring to the actual signatures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Looks great, thanks for sorting this out!

@lkdvos
lkdvos merged commit b909280 into main Aug 31, 2026
71 of 73 checks passed
@lkdvos
lkdvos deleted the ld-indexmanipulation branch August 31, 2026 22:48
lkdvos added a commit that referenced this pull request Sep 1, 2026
`add_transform_kernel!` gained a `buffersize(tsrc, fblocks)` call in #521, but
its `tdst`/`tsrc` are untyped, so the analysis widens `tsrc` to
`Union{AbstractTensorMap, HomSpace}` and the `HomSpace` arm has no `buffersize`
method. The only caller already passes tensor maps, and the two sibling methods
take `DenseVector`, so annotate the signature to match.

Also switch the `GenericTreeTransformer` matcher to `AnyFrameMethod`: #521 moved
the `@debug` into a `taskforeach` closure, so the innermost frame is now a
gensym'd closure name and `LastFrameMethod` no longer matches.

Co-Authored-By: Claude Opus 5 (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.

Regression on main: permute/braid ~1.7–1.9× slower than v0.16.5 for non-Abelian sectors

3 participants