You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
twist! scales every fusion-tree subblock separately (slow on CPU, one kernel per subblock on GPU) #595
twist! loops over fusiontrees(t) and calls scale! on every subblock. Its cost therefore grows with the number of subblocks rather than with the data size: ≈ 1–1.7 µs per subblock on the CPU, and on a CuTensorMap a separate kernel launch (≈ 4–5 µs) per subblock. Fermionic tensors with a few U(1) charges have tens of thousands of subblocks. Since tensorcontract! twists one of its operands for fermionic sectors (twistA && twist!(Anew, …) in src/tensors/tensoroperations.jl), this is paid in every such contraction and in its pullback.
Measured on an H100 NVL host, TensorKit 0.17.2, CUDA.jl 6.4.2, fℤ₂ × U(1) tensors Vχ ⊗ VD ⊗ VD' ← Vχ ⊗ VD ⊗ VD' twisted on four indices, against one elementwise product of t.data with the vector of per-entry twist factors (fastest of 10 calls after a warm-up, times in ms):
subblocks
entries
CPU twist!
CPU product
GPU twist!
GPU product
9632
154112
10.5
0.046
38.2
0.009
20000
320000
26.5
0.102
85.2
0.010
30368
273312
44.8
0.083
140.7
0.010
40736
162944
69.7
0.052
194.9
0.009
The factors depend only on the space, the twisted indices and inv; computing them costs one twist!. Cached (MemoizationKit would take care of this), a repeated twist of the same space, the common case in iterative algorithms, is a single product: 230–1300× faster on the CPU and 4000–20000× on the GPU.
In a fermionic Hubbard PEPS (fℤ₂ × U(1) × U(1), D = 6, χ = 72) twist! was 67 % of the host time of a GPU CTMRG iteration. With cached factors in a local copy of TensorKit, the Zygote gradient of the energy at a fixed environment went from 42 to 12 s on one CPU thread.
Two possible fixes:
Standalone twist!: cache the per-entry factors per (space, inds, inv), for instance with MemoizationKit.
Contractions: fold the twist into the permutation that precedes it. In tensorcontract! a twisted operand is always copied first (copyA |= twistA), and that copy is a permutation whose kernels already multiply every destination subblock by a coefficient (α * blk.coeff in the batched GPU kernel, the recoupling matrix on the CPU). The twist is diagonal in the fusion-tree basis, with a phase that depends only on the uncoupled sectors of the twisted legs of the destination tree, so a twisting variant of treebraider/treetransposer (the twisted indices in its cache key) could scale those coefficients instead. That removes the separate pass over the operand and needs no per-entry factors; it also covers non-abelian sectors (scaling the rows of the recoupling matrix). Not implemented or measured yet.
It's been on my list to fold this into the actual permutations, I'll see if I can get to this today or tomorrow.
In principle we could do more preprocessing on the data as well to see if there are larger contiguous/strided regions that share the same factor, but that's only relevant for contractions that don't include a permutation since folding it in is free, so let me start with that (this should then immediately take the fast path of the GPU implementation as well)
Activity
kshyatt commentedon Oct 26, 2026
I think this is related to our previous issue with
permute!: #508lkdvos commentedon Oct 26, 2026
It's been on my list to fold this into the actual permutations, I'll see if I can get to this today or tomorrow.
In principle we could do more preprocessing on the data as well to see if there are larger contiguous/strided regions that share the same factor, but that's only relevant for contractions that don't include a permutation since folding it in is free, so let me start with that (this should then immediately take the fast path of the GPU implementation as well)