Skip to content

Commit 054aa33

Browse files
committed
add full precompilation suite
1 parent 72fd6bf commit 054aa33

6 files changed

Lines changed: 202 additions & 135 deletions

File tree

src/TensorKit.jl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,6 @@ export notrunc, truncrank, trunctol, truncfilter, truncspace, truncerror
104104
# cache management
105105
export empty_globalcaches!
106106

107-
# precompilation
108-
export precompile_contract
109-
110107
# Imports
111108
#---------
112109
using TupleTools

src/precompile.jl

Lines changed: 33 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
using PrecompileTools: PrecompileTools, @compile_workload
2-
using Preferences: @load_preference, load_preference
1+
module Precompilation
2+
3+
export precompile_indexmanipulations, precompile_contract, precompile_factorizations
4+
5+
using ..TensorKit
6+
using ..TensorKit: TO
7+
using VectorInterface: One, Zero
8+
using TensorOperations: @tensor
9+
using PrecompileTools: @setup_workload, @compile_workload
10+
using Preferences: @load_preference
311

412
# Preferences
513
# -----------
@@ -18,150 +26,46 @@ const PRECOMPILE_ELTYPES = _validate_precompile_eltypes(
1826
@load_preference("precompile_eltypes", ["Float64", "ComplexF64"])
1927
)
2028

21-
# Tensor arities (numbers of legs) to precompile the contraction/permutation machinery for.
22-
# The heavy transform/braid machinery is specialized per arity, so this controls how many
23-
# leg-counts get the full precompilation benefit (higher arities cost more precompile time).
24-
const PRECOMPILE_NDIMS = let n = @load_preference("precompile_ndims", [2, 3, 4])
25-
(n isa Vector{Int} && all((1), n)) ||
26-
throw(ArgumentError("`precompile_ndims` should be a vector of positive `Int`s, got `$n`"))
29+
const PRECOMPILE_NDIMS = let n = @load_preference("precompile_ndims", 4)
30+
(n isa Int && n 1) ||
31+
throw(ArgumentError("`precompile_ndims` should be a positive `Int`, got `$n`"))
2732
n
2833
end
2934

30-
# Representative space for each supported sector-name in the `precompile_sectors` preference.
31-
# One representative per (fusion style, coefficient type) suffices — the heavy kernels are
32-
# sector-agnostic (see `precompile_contract`). Other sectors can be precompiled by calling
33-
# `precompile_contract` directly.
3435
function _precompile_space(name::AbstractString)
35-
name == "Trivial" && return^2
36-
name == "Z2Irrep" && return Vect[Z2Irrep](0 => 1, 1 => 1)
37-
name == "U1Irrep" && return Vect[U1Irrep](-1 => 1, 0 => 1, 1 => 1)
38-
name == "SU2Irrep" && return Vect[SU2Irrep](0 => 1, 1 // 2 => 1)
39-
name == "FermionParity" && return Vect[FermionParity](0 => 1, 1 => 1)
40-
return error("unknown precompile_sectors entry `$name`; precompile it directly with `precompile_contract`")
36+
I = Base.eval(TensorKit, Meta.parse(name))
37+
(I isa DataType && I <: Sector) ||
38+
error("invalid `precompile_sectors` entry `$name`; expected a `Sector` type")
39+
return oneunit(Vect[I])
4140
end
4241

4342
const PRECOMPILE_SECTORS = let s = @load_preference(
44-
"precompile_sectors", ["Trivial", "Z2Irrep", "SU2Irrep", "FermionParity"]
43+
"precompile_sectors", map(x -> repr(x; context = :module => TensorKit), [Trivial, Z2Irrep, SU2Irrep, FermionParity])
4544
)
4645
s isa Vector{String} ||
4746
throw(ArgumentError("`precompile_sectors` should be a vector of strings, got $(typeof(s))"))
4847
s
4948
end
5049

51-
# Whether to run the precompile workload. Respects the ecosystem-wide
52-
# `PrecompileTools` switch and a TensorKit-local `precompile_workload` preference
53-
# (default `true`). Mirrors the pattern used by TensorOperations.
54-
function _precompile_workload_enabled(mod::Module = @__MODULE__)
55-
return try
56-
if load_preference(PrecompileTools, "precompile_workloads", true)
57-
return load_preference(mod, "precompile_workload", true)
58-
else
59-
return false
60-
end
61-
catch
62-
false
63-
end
64-
end
50+
const PRECOMPILE_CONTRACT = @load_preference("precompile_contract", true)
51+
const PRECOMPILE_INDEXMANIPULATIONS = @load_preference("precompile_indexmanipulations", true)
52+
const PRECOMPILE_FACTORIZATIONS = @load_preference("precompile_factorizations", true)
6553

6654
# Workload
6755
# --------
68-
"""
69-
precompile_contract(V::IndexSpace; eltypes=[Float64, ComplexF64], ndims=[2, 3, 4])
70-
71-
Run a small, representative set of tensor operations (contraction, trace, permutation) on
72-
tensors built from the space `V`, for each element type in `eltypes` and each tensor arity
73-
(number of legs) in `ndims`. This forces compilation of the contraction/permutation machinery
74-
for the sector type of `V`.
75-
76-
The machinery is specialized per arity, so `ndims` controls which leg-counts get the full
77-
benefit; arities not covered still benefit partially from the sector- and arity-agnostic parts
78-
(most importantly the per-block BLAS `mul!`).
79-
80-
The expensive, numerically-heavy kernels (`add_transform!`, the tree transformers, and the
81-
per-block BLAS `mul!`) are *sector-agnostic* — they depend only on the element type, the
82-
arity, and `sectorscalartype(sectortype(V))`. Consequently, calling this once for a
83-
representative symmetry precompiles code that is reused by every other symmetry with the same
84-
fusion style and coefficient type.
85-
86-
Downstream packages that define their own symmetry can precompile TensorKit's contraction path
87-
for it by calling this inside their own `PrecompileTools.@compile_workload`, e.g.
88-
89-
```julia
90-
@compile_workload begin
91-
TensorKit.precompile_contract(Vect[MyAnyon](s₀ => 1, s₁ => 1))
92-
end
93-
```
94-
95-
TensorKit's own workload (enabled by default) calls this for `Trivial`, `Z2Irrep`, `SU2Irrep`
96-
and `FermionParity`. It can be tuned or disabled via preferences:
97-
98-
```julia
99-
using TensorKit, Preferences
100-
set_preferences!(TensorKit, "precompile_eltypes" => ["Float64", "ComplexF64"]; force=true)
101-
set_preferences!(TensorKit, "precompile_workload" => false; force=true) # disable entirely
102-
```
103-
"""
104-
function precompile_contract(V::IndexSpace; eltypes = PRECOMPILE_ELTYPES, ndims = PRECOMPILE_NDIMS)
105-
backend = TO.DefaultBackend()
106-
allocator = TO.DefaultAllocator()
107-
for T in eltypes
108-
α, β = rand(T), rand(T)
109-
110-
# contraction + permutation for each requested arity (leg count)
111-
for N in ndims
112-
_precompile_contract_arity(V, T, Val(N), α, β, backend, allocator)
56+
include("precompile/indexmanipulations.jl")
57+
include("precompile/contract.jl")
58+
include("precompile/factorizations.jl")
59+
60+
@setup_workload begin
61+
for name in PRECOMPILE_SECTORS
62+
V = _precompile_space(name)
63+
@compile_workload begin
64+
PRECOMPILE_INDEXMANIPULATIONS && precompile_indexmanipulations(V; eltypes = PRECOMPILE_ELTYPES)
65+
PRECOMPILE_CONTRACT && precompile_contract(V; eltypes = PRECOMPILE_ELTYPES)
66+
PRECOMPILE_FACTORIZATIONS && precompile_factorizations(V; eltypes = PRECOMPILE_ELTYPES)
11367
end
114-
115-
# the conjugated-operand branch (`conjA=true`) is a distinct runtime path (adjoint
116-
# handling) that `conj(A) * B` networks hit; `@tensor` handles the space bookkeeping
117-
A2 = randn(T, V V)
118-
B2 = randn(T, V V)
119-
@tensor Cc[a; c] := conj(A2[b; a]) * B2[b; c]
120-
121-
# partial trace (the two traced legs are mutually dual)
122-
At = randn(T, V V' V)
123-
TO.tensortrace!(
124-
TO.tensoralloc_add(T, At, ((3,), ()), false, Val(false)),
125-
At, ((3,), ()), ((1,), (2,)), false, α, β, backend, allocator
126-
)
12768
end
128-
return nothing
129-
end
130-
131-
# `V^{⊗M}` (the unit space for `M == 0`), with `M` a compile-time constant.
132-
_repeat_space(V, ::Val{0}) = one(V)
133-
_repeat_space(V, ::Val{M}) where {M} = foldl(, ntuple(_ -> V, Val(M)))
134-
135-
# Precompile the contraction and permutation machinery for tensors with `N` legs. `N` is a
136-
# `Val` so the index tuples are compile-time concrete (the machinery specializes per arity).
137-
function _precompile_contract_arity(V, ::Type{T}, ::Val{N}, α, β, backend, allocator) where {T, N}
138-
W = _repeat_space(V, Val(N - 1)) # N-1 legs
139-
# contraction of two arity-N tensors over their N-1 shared legs -> arity-2 result
140-
A = randn(T, V W)
141-
B = randn(T, W V)
142-
pA = ((1,), ntuple(i -> i + 1, Val(N - 1)))
143-
pB = (ntuple(identity, Val(N - 1)), (N,))
144-
_precompile_contract!(A, pA, B, pB, ((1,), (2,)), α, β, backend, allocator)
145-
146-
# a non-trivial permutation exercises the repartition/braid/transform machinery at arity N
147-
# (the contraction above takes the no-copy view path for its natural partition)
148-
permute(A, (ntuple(i -> N - i + 1, Val(N)), ()))
149-
return nothing
150-
end
151-
152-
# both scalar-type paths: generic `(α,β)` and the identity `(One(), Zero())` fast path
153-
function _precompile_contract!(A, pA, B, pB, pAB, α, β, backend, allocator)
154-
T = promote_type(scalartype(A), scalartype(B))
155-
C = TO.tensoralloc_contract(T, A, pA, false, B, pB, false, pAB, Val(false))
156-
TO.tensorcontract!(C, A, pA, false, B, pB, false, pAB, α, β, backend, allocator)
157-
TO.tensorcontract!(C, A, pA, false, B, pB, false, pAB, One(), Zero(), backend, allocator)
158-
return C
15969
end
16070

161-
if _precompile_workload_enabled()
162-
@compile_workload begin
163-
for name in PRECOMPILE_SECTORS
164-
precompile_contract(_precompile_space(name); eltypes = PRECOMPILE_ELTYPES)
165-
end
166-
end
167-
end
71+
end # module Precompilation

src/precompile/contract.jl

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
precompile_contract(V::IndexSpace; eltypes=[Float64, ComplexF64], ndims=4)
3+
4+
Run a small, representative set of contraction, trace, and permutation operations on tensors built
5+
from the space `V`, for each element type in `eltypes` and each tensor arity (number of legs) in `1:ndims`.
6+
7+
Note that it can be beneficial to put a more comprehensive set of relevant symmetries in a startup file,
8+
for example by adding:
9+
10+
```julia
11+
@compile_workload begin
12+
TensorKit.precompile_contract(Vect[MyAnyon](s₀ => 1, s₁ => 1))
13+
end
14+
```
15+
16+
See also [`precompile_indexmanipulations`](@ref TensorKit.precompile_indexmanipulations), [`precompile_factorizations`](@ref TensorKit.precompile_factorizations).
17+
"""
18+
function precompile_contract(V::IndexSpace; eltypes = PRECOMPILE_ELTYPES, ndims = PRECOMPILE_NDIMS)
19+
backend = TO.DefaultBackend()
20+
allocator = TO.DefaultAllocator()
21+
for T in eltypes
22+
α, β = rand(T), rand(T)
23+
24+
# contraction + permutation for each requested arity (leg count); `Val(N)`/`ntuple`
25+
# keep the index tuples concrete so the machinery specializes per arity
26+
for N in 1:ndims
27+
W = V^(N - 1) # N-1 legs
28+
# contraction of two arity-N tensors over their N-1 shared legs -> arity-2 result
29+
A = randn(T, V W)
30+
B = randn(T, W V)
31+
pA = ((1,), ntuple(i -> i + 1, Val(N - 1)))
32+
pB = (ntuple(identity, Val(N - 1)), (N,))
33+
C = TO.tensoralloc_contract(T, A, pA, false, B, pB, false, ((1,), (2,)), Val(false))
34+
# both scalar-type paths: generic `(α,β)` and the identity `(One(), Zero())` fast path
35+
TO.tensorcontract!(C, A, pA, false, B, pB, false, ((1,), (2,)), α, β, backend, allocator)
36+
TO.tensorcontract!(C, A, pA, false, B, pB, false, ((1,), (2,)), One(), Zero(), backend, allocator)
37+
38+
# a non-trivial permutation exercises the repartition/braid/transform machinery at
39+
# arity N (the contraction above takes the no-copy view path for its natural partition)
40+
permute(A, (ntuple(i -> N - i + 1, Val(N)), ()))
41+
end
42+
43+
# the conjugated-operand branch (`conjA=true`) is a distinct runtime path (adjoint
44+
# handling) that `conj(A) * B` networks hit; `@tensor` handles the space bookkeeping
45+
A2 = randn(T, V V)
46+
B2 = randn(T, V V)
47+
@tensor Cc[a; c] := conj(A2[b; a]) * B2[b; c]
48+
49+
# partial trace (the two traced legs are mutually dual)
50+
At = randn(T, V V' V)
51+
TO.tensortrace!(
52+
TO.tensoralloc_add(T, At, ((3,), ()), false, Val(false)),
53+
At, ((3,), ()), ((1,), (2,)), false, α, β, backend, allocator
54+
)
55+
end
56+
return nothing
57+
end

src/precompile/factorizations.jl

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
precompile_factorizations(V::IndexSpace; eltypes=[Float64, ComplexF64])
3+
4+
Run a small, representative set of tensor-factorization operations (singular value, QR, LQ,
5+
eigenvalue, orthogonal/null-space and polar decompositions) on tensors built from the space `V`,
6+
for each element type in `eltypes`.
7+
8+
Note that it can be beneficial to put a more comprehensive set of relevant symmetries in a startup file,
9+
for example by adding:
10+
11+
```julia
12+
@compile_workload begin
13+
TensorKit.precompile_factorizations(Vect[MyAnyon](s₀ => 1, s₁ => 1))
14+
end
15+
```
16+
17+
See also [`precompile_contract`](@ref TensorKit.precompile_contract), [`precompile_indexmanipulations`](@ref TensorKit.precompile_indexmanipulations).
18+
"""
19+
function precompile_factorizations(V::IndexSpace; eltypes = PRECOMPILE_ELTYPES)
20+
for T in eltypes
21+
# Three representative shapes: a square tensor for the bulk of the decompositions, a
22+
# hermitian one for the `eigh` path, and rectangular ones so the null-space kernels
23+
# operate on non-empty blocks.
24+
W = V^2 # V ⊗ V
25+
t = randn(T, W W) # square
26+
tr = randn(T, W V) # tall (codomain larger) -> non-empty left null space
27+
tw = randn(T, V W) # wide (domain larger) -> non-empty right null space
28+
th = (t + t') / 2 # hermitian (square, Euclidean inner product)
29+
30+
# Singular value decomposition
31+
svd_full(t)
32+
svd_compact(t)
33+
svd_vals(t)
34+
svd_trunc(t; trunc = truncrank(1))
35+
36+
# QR / LQ decompositions (null-space variants on the appropriately shaped tensors)
37+
qr_full(t)
38+
qr_compact(t)
39+
qr_null(tr)
40+
lq_full(t)
41+
lq_compact(t)
42+
lq_null(tw)
43+
44+
# Eigenvalue decompositions (hermitian variants require a hermitian input)
45+
eig_full(t)
46+
eig_vals(t)
47+
eigh_full(th)
48+
eigh_vals(th)
49+
50+
# Orthogonal / null-space helpers
51+
left_orth(t)
52+
right_orth(t)
53+
left_null(tr)
54+
right_null(tw)
55+
56+
# Polar decompositions
57+
left_polar(t)
58+
right_polar(t)
59+
end
60+
return nothing
61+
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
precompile_indexmanipulations(V::IndexSpace; eltypes=[Float64, ComplexF64], ndims=4)
3+
4+
Run a small, representative set of index-manipulation operations on tensors built from the space `V`,
5+
for each element type in `eltypes` and each tensor arity (number of legs) in `1:ndims`.
6+
7+
Note that it can be beneficial to put a more comprehensive set of relevant symmetries in a startup file,
8+
for example by adding:
9+
10+
```julia
11+
@compile_workload begin
12+
TensorKit.precompile_indexmanipulations(Vect[MyAnyon](s₀ => 1, s₁ => 1))
13+
end
14+
```
15+
16+
See also [`precompile_contract`](@ref TensorKit.precompile_contract), [`precompile_factorizations`](@ref TensorKit.precompile_factorizations).
17+
"""
18+
function precompile_indexmanipulations(V::IndexSpace; eltypes = PRECOMPILE_ELTYPES, ndims = PRECOMPILE_NDIMS)
19+
for T in eltypes
20+
# `Val(N)`/`ntuple` keep the index tuples concrete so the machinery specializes per arity
21+
for N in 1:ndims
22+
N₁ = cld(N, 2) # codomain legs
23+
# a tensor split non-trivially into codomain/domain (N₁ | N-N₁)
24+
t = randn(T, V^N₁ V^(N - N₁))
25+
26+
# a non-trivial reordering of all N legs (reverse of `1:N`), split back into (N₁ | N-N₁)
27+
perm = ntuple(i -> N - i + 1, Val(N))
28+
p1 = ntuple(i -> perm[i], Val(N₁))
29+
p2 = ntuple(i -> perm[i + N₁], Val(N - N₁))
30+
31+
# `permute` and `braid` funnel through `add_transform!` with different transformers
32+
permute(t, (p1, p2))
33+
braid(t, (p1, p2), ntuple(identity, Val(N))) # `levels` is a tuple over the source indices
34+
35+
# canonical transpose `(reverse(domain), reverse(codomain))` is always a valid cyclic transpose
36+
tp1 = ntuple(i -> N - i + 1, Val(N - N₁))
37+
tp2 = ntuple(i -> N₁ - i + 1, Val(N₁))
38+
transpose(t, (tp1, tp2))
39+
40+
# repartition to a different codomain size, forcing the repartition/transpose path
41+
repartition(t, N₁ < N ? N₁ + 1 : N₁ - 1)
42+
43+
twist(t, 1)
44+
end
45+
end
46+
return nothing
47+
end

src/tensors/indexmanipulations.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,15 +553,16 @@ Base.@deprecate(
553553
if p[1] === codomainind(tsrc) && p[2] === domainind(tsrc)
554554
add!(tdst, tsrc, α, β)
555555
else
556+
p2 = (linearize(p), ())
556557
if has_array_view(tdst) && has_array_view(tsrc)
557-
TO.tensoradd!(tdst[], tsrc[], p, false, α, β, backend, allocator)
558+
TO.tensoradd!(tdst[], tsrc[], p2, false, α, β, backend, allocator)
558559
else
559560
ntasks = use_threaded_transform(tdst, transformer) ? get_num_transformer_threads() : 1
560561
scheduler = ntasks == 1 ? SerialScheduler() : DynamicScheduler(; ntasks, split = :roundrobin)
561562
if tdst isa TensorMap && tsrc isa TensorMap # unpack data fields to avoid specializing
562-
add_transform_kernel!(tdst.data, tsrc.data, p, transformer, α, β, backend, allocator, scheduler)
563+
add_transform_kernel!(tdst.data, tsrc.data, p2, transformer, α, β, backend, allocator, scheduler)
563564
else
564-
add_transform_kernel!(tdst, tsrc, p, transformer, α, β, backend, allocator, scheduler)
565+
add_transform_kernel!(tdst, tsrc, p2, transformer, α, β, backend, allocator, scheduler)
565566
end
566567
end
567568
end

0 commit comments

Comments
 (0)