Skip to content

Precompute planar trace kernels in a TraceTransformer - #605

Open
lkdvos wants to merge 3 commits into
mainfrom
planartrace
Open

lkdvos wants to merge 3 commits into
mainfrom
planartrace

Conversation

@lkdvos

@lkdvos lkdvos commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

This replaces the per-tree loop in planartrace!, which used the fusion tree internals through ad-hoc single-pair FusionTreeBlocks, with the same precompute/execute split as the index manipulations (treetransformers.jl / indexmanipulations.jl).

Fusion trees

  • planar_trace(src::Union{FusionTreePair, FusionTreeBlock}, p, q) follows the conventions of transpose/repartition: unique-fusion pairs map to dst => coeff, blocks to dst => U with a dense recoupling matrix. It repartitions all legs to the outgoing side, traces the splitting trees into the full block of remaining trees, and transposes that block.
  • The traced sectors must be dual to each other; otherwise this throws. Callers filter with trace_sectors_match instead, which in the generic case avoids computing repartitions/transposes of blocks that vanish anyway.
  • FusionTreeBlocks now hash their uncoupled sectors and duality flags, since the ad-hoc single-pair blocks are gone.

Precompute: TraceTransformer

  • Cached via @cached treetracer(A, Vdst, Vsrc, p, q), keyed on the destination storagetype like the other transformers.
  • Stores (U, inds_dst, inds_src) entries, grouped by destination block: several source blocks (one per traced sector) contribute to the same destination. Every destination subblock appears in exactly one entry, so threaded tasks never write the same subblock, and subblocks without contributions get an entry that only scales by β.

Execute: trace_transform_kernel!

  • Mirrors add_transform_kernel!, including the per-task workspaces.
  • A single destination subblock (always the case for unique fusion) is traced into directly, with β on the first contribution.
  • Otherwise: trace each source into a buffer column (applying the output permutation while tracing), recouple with one matmul (_recouple!), and add into the destinations.

GPU kernel

  • GPU tensors pack the TraceTransformer into isbits descriptors (cached per device storagetype, like the braid/transpose transformers) and trace in two stages:

    1. partial traces: one thread per (chunk, remaining entry) of every source subblock sums at most max(256, √ntraced) traced entries (one summed stride per traced pair) into a scratch buffer from allocator, so full reductions and traces with few remaining entries are still spread over many threads;
    2. recoupling: one thread per destination element sums the partials of every source, weighted by the recoupling coefficients, so the traced entries are summed once per source rather than once per destination tree.
  • Other tensor types (e.g. adjoints) are converted to TensorMaps first. Tracing subblock views with cuTENSOR, as the previous implementation did for all GPU tensors, fails with CUDA error: misaligned address for data at unaligned offsets (reproduced for Float64 fℤ₂ ⊠ SU₂ data).

  • _linear_index is now an explicit loop, since sum(()) (a full trace) does not compile for GPUs.

  • Tested on an RTX A6000 against the CPU results for the same sectors and traces, in Float64 and ComplexF64, including adjoint sources, β = 0 with NaN-filled output, and full traces split into several chunks. test/cuda/planar.jl gains a symmetric planartrace test for every space in the spacelist and for large traces; cuda/planar and cuda/tensors pass locally.

  • GPU timings for one-stage (one thread per destination element) vs. two-stage kernel, for degeneracies 40 (fℤ₂), 30 (Fibonacci), 12/10/8 (fℤ₂ ⊠ SU₂):

    partial (3 legs left) 1 leg left full reduction
    fℤ₂ 0.28 → 0.31 ms 4.0 → 0.25 ms 4.0 → 0.19 ms
    Fibonacci 0.16 → 0.20 ms 2.8 → 0.21 ms 2.8 → 0.18 ms
    fℤ₂ ⊠ SU₂ 0.042 → 0.036 ms 0.81 → 0.12 ms 0.81 → 0.12 ms

Testing

  • Compared against the previous implementation for fℤ₂, Fibonacci, fℤ₂ ⊠ SU₂ and Fibonacci ⊠ fℤ₂, with partial, full and nested traces, adjoint sources, and the tree pair / block coefficients.
  • tensors/planar, tensors/contractions and symmetries/doubletree pass locally; the planar trace test in doubletree.jl now uses the block method for generic fusion.
  • Rough timings for a 5-leg trace (warm cache): fℤ₂ 0.39 → 0.26 ms, Fibonacci 0.32 → 0.058 ms, fℤ₂ ⊠ SU₂ 1.9 → 0.036 ms.

Not covered: JET was not run locally (it passes in CI); the GPU kernels were only tested on CUDA, not AMDGPU.

🤖 Generated with Claude Code

lkdvos and others added 3 commits October 30, 2026 11:40
`planartrace!` now precomputes the subblock positions and recoupling
coefficients in a cached `TraceTransformer`, and executes them with a
sector-independent kernel, analogous to the index manipulations. This
replaces the per-tree loop that built ad-hoc single-pair `FusionTreeBlock`s.

- `planar_trace` is defined for `Union{FusionTreePair, FusionTreeBlock}`,
  with unique-fusion pairs returning `dst => coeff` and blocks `dst => U`,
  and throws when the traced sectors are not dual to each other
- the transformer skips such (sub)blocks upfront and groups the data by
  destination, such that every destination subblock is written once and
  `β` is handled inside the kernel
- blocks with several destination trees are traced (and permuted) into a
  buffer, recoupled with a single matmul and added into the destination
- `FusionTreeBlock`s hash their uncoupled sectors and duality flags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU tensors pack the `TraceTransformer` into `isbits` descriptors, cached
per device storagetype, and trace with one thread per destination element,
fusing the sum over the traced entries and the recoupling into a single
kernel, analogous to the generic tree transformers.

Other tensor types, such as adjoints, are first converted to `TensorMap`s:
tracing subblock views with cuTENSOR fails for data offsets that are not
aligned (e.g. `Float64` data at odd offsets).

`_linear_index` is now written as an explicit loop, since `sum` of an empty
tuple, as occurs for a full trace, does not compile for GPUs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first stage sums chunks of at most `max(256, √ntraced)` traced entries per thread, so
full reductions and traces with few remaining entries are still spread over many threads.
The second stage recouples these partials with one thread per destination element, such
that traced entries are summed once per source rather than once per destination tree.

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

This branch has not been deployed

No deployments
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.

1 participant