Skip to content

Cache F- and R-symbols and direct products with MemoizationKit - #59

Draft
lkdvos wants to merge 4 commits into
mainfrom
ld/memoizationkit
Draft

lkdvos wants to merge 4 commits into
mainfrom
ld/memoizationkit

Conversation

@lkdvos

@lkdvos lkdvos commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Stacked on #58 (base branch ld/sorted-directproduct); the diff below only contains the commit on top of it. An alternative to the caches of #57, using MemoizationKit.jl.

The F- and R-symbols were cached in one LRU per rank, which takes a lock on every lookup, hits included, so TensorKit reading them from many threads serializes on it. #57 replaces this by a snapshot cache and a 64-shard LRU. Here, the caches are @cached functions instead:

  • Fsymbol and Rsymbol call @cached functions fsymbol and rsymbol (the former _Fsymbol and _Rsymbol). These are owned by SUNRepresentations, so their size can be set per function in the [SUNRepresentations.MemoizationKit.fsymbol] preferences, and they show up in MemoizationKit's cache_info and dashboard.
  • sorteddirectproduct, which ⊗ iterates over, is @cached as well. Its result is shared and must not be mutated. Nsymbol stays uncached: most calls are rejected by its cheap checks in about 5 ns, and the others are comparable to a cache lookup for small products.
  • MemoizationKit's default container, ClockCache, is read without a lock on the lockfree-clock branch (Make ClockCache hits lock-free MemoizationKit.jl#21), which is selected through a [sources] entry until that is released.

The CGC cache is unchanged: it is only read when F- and R-symbols are computed, which are now cached.

Benchmarks

The reproducer of #56/#57 (cold construction of the recoupling data of one permutation in a CTMRG step of an SU(3)-symmetric PEPS), minimum over two interleaved rounds. "#58 + #57" applies the caches of #57 on top of #58, with the snapshot cache holding the sorted direct product of ⊗:

main #56 #57 #58 #58 + #57 this PR
cold permute, 1 thread 5.07 s 1.85 s 0.80 s 1.24 s 0.68 s 0.69 s
cold permute, 8 threads 1.27 s 0.62 s 0.19 s 0.41 s 0.18 s 0.16 s

End to end, MemoizationKit matches the dedicated caches of #57. The difference shows in the cost of a cache hit (@belapsed), and when many threads hit at once (8 tasks, each looking up the same 2404 warm SU(3) F-symbols, time per lookup):

#58 (LRU per rank) #57 (sharded LRU) #58 + #57 this PR
Fsymbol hit 136 ns 138 ns 141 ns 39 ns
Rsymbol hit 111 ns 70 ns 74 ns 25 ns
Fsymbol hits, 8 tasks 542 ns 168 ns 145 ns 19 ns
iterate (1, 1) ⊗ (2, 1) 221 ns 35 ns 24 ns 38 ns

The lock-free ClockCache makes F- and R-symbol hits 3–4× cheaper than the sharded LRU, and these hits scale with threads (19 ns per lookup over 8 tasks, against 116 ns for 1 task on the same keys). Iterating over ⊗ is about 14 ns slower than with the snapshot cache, whose hits are a plain Dict lookup in an immutable snapshot (at the cost of copying the snapshot on every insertion).

Details and caveats
  • The reproducer creates 1501 F-symbols, 209 R-symbols and 291 direct products, well within MemoizationKit's default limit of 10000 entries per function. The previous caches held up to 10⁵ entries per rank. MemoizationKit's main lets a package ship its own defaults (default_cache_settings, Package-provided default cache settings per function MemoizationKit.jl#20), but the lockfree-clock branch predates that; with both, this PR could keep the previous limits.
  • The F-symbol cache counted 1901 misses for its 1501 entries: computations run outside the lock, so threads that miss the same key at the same time compute it more than once. Cache direct products, and read the symbol caches without locking #57 has the same behaviour.
  • Julia 1.10 ignores [sources], and resolves the registered MemoizationKit 0.1.0, whose ClockCache locks on hits (correct, but without the scaling above).
  • LRUCache is still used for the CGC cache.

TensorKit v0.17.2, TensorKitSectors v0.3.10, Julia 1.13.1, shared 32-core workstation (load average 12–16 during these runs; the tables use the minimum).

🤖 Generated with Claude Code

leburgel and others added 4 commits October 14, 2026 17:03
The Weyl dimension formula used `Rational` arithmetic. The factors with indices up to `k2` give the dimension of an SU(k2) irrep, so the product of each row of factors can be divided exactly in integer arithmetic (with an `Int128` intermediate), which keeps the intermediate values close to the result. Tested against the rational formula up to SU(12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- For N ≥ 6, irreps with few boxes (or whose dual has few boxes) use the hook-content formula,
  which costs O(number of boxes) instead of O(N²). Its numerator is bounded up front, which
  selects exact Int, Int128 or BigInt arithmetic.
- The Weyl formula bounds the product of every row of factors, and switches to BigInt when it
  could overflow an Int128. Since Dynkin labels are at most 255, no checks are needed for
  N ≤ 6. Previously, irreps of SU(N ≥ 22) could throw an `AssertionError` from the wrapped
  products. Dimensions that do not fit in an `Int` throw an `OverflowError`.
- `weight` is computed in O(N) instead of O(N²).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`s1 ⊗ s2` iterated the keys of a `Dict`, so the order of fusion products, and with it the
fusion tree order and data layout of SU(N) tensors, depended on `hash(::SUNIrrep)`. The
direct product is now returned in sorted (`isless`) order, which coincides with the order of
`values(SUNIrrep{N})`. Fixes #54.

- Direct products are computed with a pruned Littlewood-Richardson enumeration in GT form,
  which only visits patterns that contribute. `Nsymbol` uses the same search restricted to a
  single target irrep, after cheap box-count and containment checks (10-1000x faster).
- `directproduct` counts multiplicities in a `Dict` during the enumeration. For `⊗`, they are
  counted by sorting for small products, and in a `Dict` for large ones.

Note that the order of `⊗` changes once, so tensor data stored with the previous layout must
be converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The F- and R-symbols were cached in one `LRU` per rank, which takes a lock on every lookup,
hits included, so that TensorKit reading them from many threads serializes on it. They are now
`@cached` with MemoizationKit, whose default `ClockCache` is read without a lock (on the
`lockfree-clock` branch, selected through `[sources]`). The sorted direct product behind `⊗` is
cached in the same way. `Nsymbol` stays uncached: most calls are rejected by its cheap checks in
about 5 ns, and the others are comparable to a cache lookup for small products.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/SUNRepresentations.jl 100.00% <ø> (ø)
src/gtpatterns.jl 96.08% <100.00%> (ø)
src/sector.jl 93.51% <100.00%> (-0.75%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lkdvos
lkdvos force-pushed the ld/sorted-directproduct branch from 8beb0d7 to cba7abe Compare October 15, 2026 15:35
Base automatically changed from ld/sorted-directproduct to main October 16, 2026 12:23
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