Skip to content

Refactor simple update and reduce weight-absorption allocations - #459

Merged
Yue-Zhengyuan merged 8 commits into
QuantumKitHub:mainfrom
Yue-Zhengyuan:simpleupdate-refactor
Oct 29, 2026
Merged

Yue-Zhengyuan merged 8 commits into
QuantumKitHub:mainfrom
Yue-Zhengyuan:simpleupdate-refactor

Conversation

@Yue-Zhengyuan

Copy link
Copy Markdown
Member

This PR further improves performance of simple update, with a few more refactoring.

  • Make internal _absorb_weight accept more flexible tensor leg order, avoiding intermediate permutations between PEPS/PEPO and MPS (cluster) order.
  • Simplify bond-direction and rotation handling.
  • Extract a few helper functions on canonicalizing the MPS cluster.
  • Add iter/t keywords to restart timestep through the public API.

@Yue-Zhengyuan
Yue-Zhengyuan requested a review from lkdvos October 16, 2026 08:47
@Yue-Zhengyuan

Copy link
Copy Markdown
Member Author

Some of the refactors are peeled off from #144, so this one should go first.

@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.15966% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/algorithms/time_evolution/simpleupdate.jl 96.15% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/PEPSKit.jl 100.00% <ø> (ø)
src/algorithms/contractions/absorb_weight.jl 100.00% <100.00%> (ø)
src/algorithms/time_evolution/apply_gate.jl 92.50% <100.00%> (+7.50%) ⬆️
src/algorithms/time_evolution/apply_mpo.jl 100.00% <100.00%> (ø)
src/algorithms/time_evolution/get_cluster.jl 96.42% <100.00%> (+0.59%) ⬆️
src/algorithms/time_evolution/simpleupdate3site.jl 90.19% <100.00%> (+0.61%) ⬆️
src/utility/util.jl 85.54% <100.00%> (-0.35%) ⬇️
src/algorithms/time_evolution/simpleupdate.jl 97.05% <96.15%> (-0.03%) ⬇️

... and 2 files with indirect coverage changes

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

Yue-Zhengyuan and others added 6 commits October 28, 2026 18:32
Rework the nearest-neighbor simple update along the lines of the Canopy.jl
two-site kernel:

- Absorb diagonal bond weights directly on each fusion-tree block, in the
  native leg order, with a single fused Strided broadcast. This replaces the
  permute-multiply-permute loop and the `input_axes`/`output_perm` keyword
  arguments of `_absorb_weight`.
- Update a bond without rotating the tensors: derive the legs from the bond
  direction, split each site with one permutation and a QR (`_bond_qr`), apply
  the gate to the reduced tensors (`_apply_bond_gate`), and contract the result
  straight back into the native leg order (`_undo_bond_qr`). Orient the bond
  such that the SVD reproduces the original arrow, so no flips are needed.
- Serve temporaries from `MPSKit.default_allocator`, created once per
  `su_iter`. Temporaries never outlive the function that allocates them.

Remove the now unused `_bond_rotation`, `_absorb_weight!`, `_permute_cluster`,
`_permute_to_last` and `_apply_gate`, add `absorb_weight!` and `absorb_s!`,
and require TensorOperations 5.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a `timeevol` benchmark module that times one `su_iter` sweep over a
2 x 2 unit cell for trivial and U(1) Heisenberg PEPS, fermionic Hubbard
PEPS and a Heisenberg PEPO, at two bond dimensions each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per fusion-tree block, `block(wt, c)` and `t[f₁, f₂]` each redo a lookup in
TensorKit's global structure cache, which dominated the cost of absorbing
weights into tensors with many small blocks. Look up the weight blocks once
per call and iterate with `subblocks` instead. Also make the tensor rank
static in the `ntuple` that reshapes the weights, which was otherwise not
inferred and caused dynamic dispatch on every broadcast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lkdvos
lkdvos force-pushed the simpleupdate-refactor branch from a8ae4e3 to 0e1c18e Compare October 28, 2026 22:45
@lkdvos

lkdvos commented Oct 28, 2026

Copy link
Copy Markdown
Member

I pushed a few commits on top of this PR, taking inspiration from the two-site kernel in Canopy.jl, and rebased the branch onto main.

Changes

  • Weight absorption without permutations. _absorb_weights! scales the virtual legs directly on every fusion-tree block, in the native leg order, with a single fused Strided broadcast for all weights. This replaces the permute-multiply-permute loop, together with the input_axes/output_perm keyword arguments of _absorb_weight. The three-site update now uses permute(absorb_weight(...), perm) and absorb_weight!(permute(M, invperm), ...; inv = true) instead.
  • Rotation-free nearest-neighbor update. The legs are derived from the bond direction, so the tensors are no longer rotated. Each site is permuted once and split by a QR (_bond_qr), the gate is applied to the reduced tensors (_apply_bond_gate), and the result is contracted straight back into the native leg order (_undo_bond_qr). The bond is oriented such that the SVD reproduces the original arrow, so no flips are needed afterwards. This removes _bond_rotation, _apply_gate, _absorb_weight!, _permute_cluster and _permute_to_last.
  • Buffered temporaries. su_iter creates one allocator per iteration with MPSKit.default_allocator(state, SerialScheduler()), which serves the temporaries of the bond updates. Temporaries never outlive the function that allocates them.
  • Benchmarks. A new timeevol module in benchmark/ times one su_iter sweep over a 2 x 2 unit cell.

Benchmarks

One su_iter sweep over a 2 x 2 unit cell, minimum over up to 20 samples. "main" is 04d9fde (the base this PR was merged with), "PR" is a8ae4e3 (this PR before my commits), "new" is the PR with my commits (before the rebase onto main).

case time main time PR time new new vs main new vs PR memory main → PR → new
Heisenberg PEPO / Trivial / D4 5.7 ms 5.3 ms 4.6 ms 1.22× 1.14× 6.7 MiB → 5.9 MiB → 2.8 MiB
Heisenberg PEPO / Trivial / D6 14.0 ms 13.2 ms 11.9 ms 1.18× 1.11× 28.1 MiB → 24.3 MiB → 8.8 MiB
Heisenberg PEPS / Trivial / D16 214.6 ms 142.2 ms 117.6 ms 1.82× 1.21× 487.0 MiB → 391.0 MiB → 104.0 MiB
Heisenberg PEPS / Trivial / D8 11.7 ms 11.1 ms 8.7 ms 1.36× 1.28× 32.2 MiB → 26.2 MiB → 7.9 MiB
Heisenberg PEPS / U1 / D16 30.1 ms 27.6 ms 16.3 ms 1.84× 1.69× 87.8 MiB → 72.2 MiB → 17.3 MiB
Heisenberg PEPS / U1 / D8 11.1 ms 10.5 ms 7.5 ms 1.50× 1.41× 8.0 MiB → 7.0 MiB → 3.7 MiB
Hubbard PEPS / fZ2xU1 / D16 91.9 ms 99.6 ms 55.8 ms 1.65× 1.78× 179.9 MiB → 148.5 MiB → 37.0 MiB
Hubbard PEPS / fZ2xU1 / D8 18.4 ms 16.9 ms 12.1 ms 1.53× 1.40× 15.2 MiB → 13.3 MiB → 6.3 MiB

Allocation counts (su_iter, new vs PR) also went down in every case, e.g. 32623 → 26784 for Hubbard D=8. Timings are from a shared workstation and fluctuate by about 10-20% between runs; the memory numbers are exact.

Testing

I checked that the new kernel reproduces the _su_iter! of this PR for every nearest-neighbor gate in both site orders: same leg spaces and weights, and the same contraction of the two updated tensors over their bond (up to ~1e-15). The cases covered were trivial and U(1) PEPS, and fermionic PEPS and PEPO (purified = true and false), all with random virtual arrows. The timestep, site-dependent truncation, SU weight and bond environment tests passed locally before the rebase; the full time-evolution tests on the rebased branch are still running, and GPU is untested.

🤖 Generated with Claude Code

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

I tried to include some cleanup as well, but might have removed some of the helper functions you still needed, I do apologize if this is the case, let me know which and I'll try adding them back. Otherwise, I'd say this is definitely an improvement, and overall the code didn't really get all that much more complicated, the added lines are mostly due to adding the benchmark cases so I'd say that's not too bad.

Otherwise good to go from my end!

@Yue-Zhengyuan

Copy link
Copy Markdown
Member Author

I'll see if I can adapt #144 cleanly before deciding if any of the rotation helpers need to stay.

@Yue-Zhengyuan
Yue-Zhengyuan merged commit 83b4955 into QuantumKitHub:main Oct 29, 2026
31 of 32 checks passed
@Yue-Zhengyuan
Yue-Zhengyuan deleted the simpleupdate-refactor branch October 29, 2026 06:41
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.

2 participants