Skip to content

Commit 72fd6bf

Browse files
lkdvosclaude
andcommitted
Precompile the tensor-contraction path via a PrecompileTools workload
Add `src/precompile.jl`: a default-on `@compile_workload` that runs representative contractions, traces and permutations for `Trivial`, `Z2Irrep`, `SU2Irrep` and `FermionParity` over `Float64`/`ComplexF64` and arities [2,3,4]. The heavy kernels (tree transformers, `add_transform!`, per-block BLAS `mul!`) are sector-agnostic, so this also speeds up the first contraction of other symmetries -- including user-defined ones. The exported `precompile_contract(V; eltypes, ndims)` helper is the reusable unit; downstream packages can call it for their own symmetry inside their own `@compile_workload`. The workload is tunable/disable-able through Preferences (`precompile_workload`, `precompile_eltypes`, `precompile_sectors`, `precompile_ndims`), mirroring TensorOperations. Also move `_repartition_body` above the `@generated repartition` that calls it at generation time: the workload is the first code to trigger `repartition` at precompile time, which exposed a latent world-age ordering bug (`UndefVarError` that runtime masks via a later world age). Adds PrecompileTools and Preferences dependencies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cfaa073 commit 72fd6bf

5 files changed

Lines changed: 225 additions & 3 deletions

File tree

Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
1313
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1414
MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4"
1515
OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5"
16+
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
17+
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
1618
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1719
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1820
ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63"
@@ -57,6 +59,8 @@ LinearAlgebra = "1"
5759
MatrixAlgebraKit = "0.6.8"
5860
Mooncake = "0.5.27"
5961
OhMyThreads = "0.8.0"
62+
PrecompileTools = "1"
63+
Preferences = "1"
6064
Printf = "1"
6165
Random = "1"
6266
ScopedValues = "1.3.0"

docs/src/man/contractions.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,45 @@ Since in this case `@tensor` and `@planar` yield different results, the `@planso
317317

318318

319319
[^Mortier]: Mortier, Q., Devos, L., Burgelman, L., et al. (2025). Fermionic Tensor Network Methods. SciPost Physics 18, no. 1. [10.21468/SciPostPhys.18.1.012](https://doi.org/10.21468/SciPostPhys.18.1.012).
320+
321+
## Precompilation
322+
323+
The first tensor contraction in a Julia session incurs a significant compilation latency
324+
(time-to-first-execution, TTFX). To mitigate this, TensorKit ships a precompilation workload
325+
that is **enabled by default**: it runs a small set of representative contractions, traces and
326+
permutations for the `Trivial`, `Z2Irrep`, `SU2Irrep` and `FermionParity` symmetries over
327+
`Float64` and `ComplexF64`.
328+
329+
Because the numerically-heavy kernels are *sector-agnostic* (they depend only on the element
330+
type, the arity, and `sectorscalartype`), this workload also speeds up the first contraction of
331+
symmetries that are *not* in the workload — including user-defined ones.
332+
333+
The workload can be tuned or disabled through [Preferences](https://github.com/JuliaPackaging/Preferences.jl):
334+
335+
```julia
336+
using TensorKit, Preferences
337+
# disable the workload entirely (fastest precompilation, no TTFX benefit)
338+
set_preferences!(TensorKit, "precompile_workload" => false; force=true)
339+
# restrict the element types
340+
set_preferences!(TensorKit, "precompile_eltypes" => ["Float64"]; force=true)
341+
# restrict / change the symmetries that are precompiled
342+
set_preferences!(TensorKit, "precompile_sectors" => ["Trivial", "Z2Irrep"]; force=true)
343+
# change the tensor arities (leg counts) that are precompiled, e.g. up to rank-6 for PEPS/PEPO
344+
set_preferences!(TensorKit, "precompile_ndims" => [2, 3, 4, 5, 6]; force=true)
345+
```
346+
347+
Changing a preference triggers recompilation of TensorKit the next time it is loaded.
348+
349+
Downstream packages that define their own symmetry can precompile TensorKit's contraction path
350+
for it by calling [`precompile_contract`](@ref) inside their own `PrecompileTools.@compile_workload`:
351+
352+
```julia
353+
using TensorKit, PrecompileTools
354+
@compile_workload begin
355+
TensorKit.precompile_contract(Vect[MyAnyon](s₀ => 1, s₁ => 1))
356+
end
357+
```
358+
359+
```@docs; canonical=false
360+
precompile_contract
361+
```

src/TensorKit.jl

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

107+
# precompilation
108+
export precompile_contract
109+
107110
# Imports
108111
#---------
109112
using TupleTools
@@ -288,4 +291,6 @@ include("pullbacks/tensoroperations.jl")
288291

289292
include("pullbacks/indexmanipulations.jl")
290293

294+
include("precompile.jl")
295+
291296
end

src/fusiontrees/duality_manipulations.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,10 @@ U = Utmp * U
474474
return dst, U
475475
```
476476
=#
477-
@generated function repartition(src::Union{FusionTreePair, FusionTreeBlock}, ::Val{N}) where {N}
478-
return _repartition_body(numout(src) - N)
479-
end
477+
# NOTE: `_repartition_body` must be defined *before* the `@generated repartition` that calls
478+
# it, so that it is visible to the generator's world age. Otherwise a precompile workload that
479+
# first triggers `repartition` fails with `UndefVarError: _repartition_body` (at runtime the
480+
# first call happens at a later world age, which masks the ordering issue).
480481
function _repartition_body(N)
481482
if N == 0
482483
ex = quote
@@ -503,6 +504,9 @@ function _repartition_body(N)
503504
end
504505
return ex
505506
end
507+
@generated function repartition(src::Union{FusionTreePair, FusionTreeBlock}, ::Val{N}) where {N}
508+
return _repartition_body(numout(src) - N)
509+
end
506510

507511
"""
508512
transpose((f₁, f₂)::FusionTreePair{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂}

src/precompile.jl

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using PrecompileTools: PrecompileTools, @compile_workload
2+
using Preferences: @load_preference, load_preference
3+
4+
# Preferences
5+
# -----------
6+
function _validate_precompile_eltypes(eltypes)
7+
eltypes isa Vector{String} ||
8+
throw(ArgumentError("`precompile_eltypes` should be a vector of strings, got $(typeof(eltypes)) instead"))
9+
return map(eltypes) do Tstr
10+
T = eval(Meta.parse(Tstr))
11+
(T isa DataType && T <: Number) ||
12+
error("Invalid precompile_eltypes entry: `$Tstr`")
13+
return T
14+
end
15+
end
16+
17+
const PRECOMPILE_ELTYPES = _validate_precompile_eltypes(
18+
@load_preference("precompile_eltypes", ["Float64", "ComplexF64"])
19+
)
20+
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`"))
27+
n
28+
end
29+
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.
34+
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`")
41+
end
42+
43+
const PRECOMPILE_SECTORS = let s = @load_preference(
44+
"precompile_sectors", ["Trivial", "Z2Irrep", "SU2Irrep", "FermionParity"]
45+
)
46+
s isa Vector{String} ||
47+
throw(ArgumentError("`precompile_sectors` should be a vector of strings, got $(typeof(s))"))
48+
s
49+
end
50+
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
65+
66+
# Workload
67+
# --------
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)
113+
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+
)
127+
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
159+
end
160+
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

0 commit comments

Comments
 (0)