Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ projects = ["test", "docs"]
[deps]
Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4"
LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4"
MemoizationKit = "1b238080-9255-4fe9-b224-89eb24efe93b"
OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
Expand Down Expand Up @@ -55,9 +55,9 @@ Enzyme = "0.13.216"
EnzymeTestUtils = "0.2.8"
FiniteDifferences = "0.12"
GPUArrays = "11.4.1"
LRUCache = "1.6"
LinearAlgebra = "1"
MatrixAlgebraKit = "0.6.14"
MemoizationKit = "0.1.2"
Mooncake = "0.5.27"
OhMyThreads = "0.8.0"
PrecompileTools = "1"
Expand Down
1 change: 1 addition & 0 deletions docs/src/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec
### Changed

- `planartrace!` precomputes and caches a `TraceTransformer` with the subblock positions and recoupling coefficients, and executes it with a kernel shared by all sectortypes, analogous to the index manipulations; `planar_trace` is now also defined for `FusionTreeBlock`s, and GPU tensors use a dedicated kernel (internal)
- Caching is provided by [MemoizationKit.jl](https://github.com/QuantumKitHub/MemoizationKit.jl), which can inspect, resize and configure TensorKit's caches, also by memory size; this replaces the internal `@cached` machinery and the LRUCache.jl dependency

### Deprecated

Expand Down
44 changes: 43 additions & 1 deletion docs/src/man/fusiontrees.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,49 @@ This is simply given by

The `braid` and `permute` routines for double fusion trees will be the main access point for corresponding manipulations on tensors.
As a consequence, results from these routines are memoized via a `@cached` mechanism.
The caching strategy is controlled by `CacheStyle`: for `FusionStyle(I) isa UniqueFusion` no caching is used (`NoCache()`), since the result is a single cheap scalar coefficient; for `MultipleFusion` or `GenericFusion` sectors, a global LRU cache (`GlobalLRUCache()`) is used, as computing the transformation matrix can be expensive and results are reused frequently.
The caching strategy is controlled by `MemoizationKit.CacheStyle`: for `FusionStyle(I) isa UniqueFusion` no caching is used (`NoCache()`), since the result is a single cheap scalar coefficient; for `MultipleFusion` or `GenericFusion` sectors, `GlobalCache()` uses MemoizationKit.jl's configured container (ClockCache by default), as computing the transformation matrix can be expensive and results are reused frequently.

TensorKit also uses MemoizationKit.jl for sector structures, degeneracy structures and fusion tree transformers.
Sector structures share cache entries across spaces with the same sectors, even if their degeneracy dimensions differ, through `MemoizationKit.cachekey`.
The MemoizationKit APIs can inspect, resize and configure these caches:

```julia
using TensorKit, MemoizationKit
MemoizationKit.cache_info(TensorKit)
MemoizationKit.set_cache_size!(TensorKit.treebraider, 50_000)
MemoizationKit.set_cache_size!(TensorKit.treebraider, 2^30; by = MemoizationKit.cachesize) # 1 GiB of cached values
MemoizationKit.set_cache_preferences!(TensorKit; maxsize = 50_000, measure = "bytes")
MemoizationKit.set_cache_preferences!(; container = "LRU")
```

Preferences are saved in `LocalPreferences.toml` and apply after restarting Julia; a runtime size change applies immediately.
`MemoizationKit.uncached(f, args...)` computes a result without consulting or filling the cache.
TensorKit supplies efficient `MemoizationKit.cachesize` methods for its structure, fusion-tree and transformer results.
These estimate bytes from the fixed-size fields and live array buffers; matrix coefficients and inline sector values are not traversed individually.
Reference-containing scalar or sector arrays fall back to `Base.summarysize`.
Buffers shared between cache entries count toward each entry, while identical source and destination structure buffers within a transformer count once.
The budget excludes cache keys, container and allocation overhead, and spare array capacity; it is a cached-value estimate rather than a limit on total process memory.
Further features, such as the live cache dashboard, disk caching and the full configuration options, are described in the [MemoizationKit.jl documentation](https://quantumkithub.github.io/MemoizationKit.jl/stable/).

For example, `using Tachikoma; MemoizationKit.cache_dashboard()` shows the hit rates and sizes of TensorKit's caches live, here after a few index manipulations on tensors with different symmetries:

```text
[RAM] │ Disk
5 caches · 184 entries · recent hit rate 77.5%
Name ▲ Hit rate Size Kind Activity
▌TensorKit.degeneracystructure █████97%████▋ ▏░░░░░░░12/10k░░░░░░░░ Clock ▇███▇███
TensorKit.fusiontreebraid ░░░░░0%░░░░░░ ▍░░░░░░147/10k░░░░░░░░ Clock
TensorKit.sectorstructure █████96%████▌ ▏░░░░░░░12/10k░░░░░░░░ Clock ████████
TensorKit.treebraider █████81%██▌░░ ▏░░░░░░░9/10k░░░░░░░░░ Clock ███ ███
TensorKit.treetwister █████80%██▍░░ ▏░░░░░░░4/10k░░░░░░░░░ Clock ████████

╭─ TensorKit.degeneracystructure ────────────────────────────────────────────────────────────────╮
│Clock · 12 of 10000 entries · 278 hits · 12 misses │
│hit rate: recent 97.5% · lifetime 95.9% · 23.0 hits/s │
│███▅▇▇▇▅▇▇▇ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
⇥ tab ↑↓ select ⏎ resize e empty s sort r reverse / filter g refresh q quit
```

This caching implies that potential inefficiencies in the fusion tree manipulations (which we nonetheless try to avoid) will not seriously affect performance of tensor manipulations.

Expand Down
1 change: 1 addition & 0 deletions docs/src/man/profiling.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# [Profiling and timers](@id s_profiling)

TensorKit's index manipulations, tensor contractions and factorizations are instrumented with [TimerOutputs.jl](https://github.com/KristofferC/TimerOutputs.jl) sections that are compiled away by default, so they incur no runtime cost.
Enabling TensorKit's timers also enables MemoizationKit.jl's cache lookup and computation timers, recorded in the same timer tree and categories.
They can be enabled to obtain a detailed breakdown of where time is spent inside these operations, in particular the split between the different kinds of work involved in manipulating symmetric tensors:

| category | contents |
Expand Down
12 changes: 7 additions & 5 deletions ext/TensorKitEnzymeExt/utility.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pullback_dC!(ΔC, β::Number) = scale!(ΔC, conj(β))
@inline EnzymeRules.inactive_type(::Type{<:TensorKit.GenericTreeTransformer}) = true
@inline EnzymeRules.inactive_type(::Type{<:TensorKit.UniqueTreeTransformer}) = true
@inline EnzymeRules.inactive_type(::Type{<:TensorKit.VectorSpace}) = true
@inline EnzymeRules.inactive_type(::Type{<:TensorKit.LRU}) = true
@inline EnzymeRules.inactive_type(::Type{<:TensorKit.MemoizationKit.AbstractCache}) = true

function EnzymeRules.augmented_primal(
config::EnzymeRules.RevConfigWidth{1},
Expand Down Expand Up @@ -142,16 +142,18 @@ end
@inline EnzymeRules.inactive(::typeof(TensorKit.fusiontrees), ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fusiontrees), ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fusiontrees), ::Any, ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fsbraid), ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fsbraid), ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fusiontreebraid), ::Any, ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.artin_braid), ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fstranspose), ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fstranspose), ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.fusiontreetranspose), ::Any, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.product_transform), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.treetransposer), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.treebraider), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.treetracer), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.treetwister), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.device_treetransposer), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.device_treebraider), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.device_treetracer), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.device_treetwister), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.insertleftunit), ::HomSpace, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.insertrightunit), ::HomSpace, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.removeunit), ::HomSpace, ::Any) = nothing
Expand Down
64 changes: 26 additions & 38 deletions ext/TensorKitGPUArraysExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -371,65 +371,52 @@ function DeviceTraceTransformer(transformer::TraceTransformer{T, N₁, N₂}, p,
)
end

TensorKit.treebraider(
A::Type{<:AnyGPUVector}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, levels::IndexTuple
) = TensorKit.device_treebraider(A, Vdst, Vsrc, p, conjsrc, levels)
TensorKit.treetransposer(
A::Type{<:AnyGPUVector}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, conjsrc::Bool
) = TensorKit.device_treetransposer(A, Vdst, Vsrc, p, conjsrc)
TensorKit.treetracer(
A::Type{<:AnyGPUVector}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, q::Index2Tuple
) = TensorKit.device_treetracer(A, Vdst, Vsrc, p, q)
TensorKit.treetwister(
A::Type{<:AnyGPUVector}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, twistmask::NTuple{N, Bool}, inv::Bool
) where {N} = TensorKit.device_treetwister(A, Vdst, Vsrc, p, conjsrc, twistmask, inv)

# Cache the device transformer directly, constructing the cached host version only on a miss.
@cached function TensorKit.treebraider(
@cached function TensorKit.device_treebraider(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, levels::IndexTuple
) where {TA <: AnyGPUVector}
host = TensorKit.treebraider(Vector{scalartype(A)}, Vdst, Vsrc, p, conjsrc, levels)
return device_transformer(A, host, linearize(p))
end

@cached function TensorKit.treetransposer(
@cached function TensorKit.device_treetransposer(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, conjsrc::Bool
) where {TA <: AnyGPUVector}
host = TensorKit.treetransposer(Vector{scalartype(A)}, Vdst, Vsrc, p, conjsrc)
return device_transformer(A, host, linearize(p))
end

@cached function TensorKit.treetracer(
@cached function TensorKit.device_treetracer(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, q::Index2Tuple
) where {TA <: AnyGPUVector}
host = TensorKit.treetracer(Vector{scalartype(A)}, Vdst, Vsrc, p, q)
return device_transformer(A, host, p, q)
end

@cached function TensorKit.treetwister(
@cached function TensorKit.device_treetwister(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, twistmask::NTuple{N, Bool}, inv::Bool
) where {TA <: AnyGPUVector, N}
host = TensorKit.treetwister(Vector{scalartype(A)}, Vdst, Vsrc, p, conjsrc, twistmask, inv)
return device_transformer(A, host, linearize(p))
end

const DEFAULT_DEVICECACHE_SIZE = 1000

function __init__()
# extensions can't modify GLOBAL_CACHES during precompilation, so register explicitly
append!(
TensorKit.GLOBAL_CACHES, (
Symbol("TensorKitGPUArraysExt.GLOBAL_TREEBRAIDER_CACHE") => GLOBAL_TREEBRAIDER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_TREETRANSPOSER_CACHE") => GLOBAL_TREETRANSPOSER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_DIAGONAL_TREEBRAIDER_CACHE") => GLOBAL_DIAGONAL_TREEBRAIDER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_DIAGONAL_TREETRANSPOSER_CACHE") => GLOBAL_DIAGONAL_TREETRANSPOSER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_DIAGONAL_TREETWISTER_CACHE") => GLOBAL_DIAGONAL_TREETWISTER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_TREETRACER_CACHE") => GLOBAL_TREETRACER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_TREETWISTER_CACHE") => GLOBAL_TREETWISTER_CACHE,
)
)

# device-caches should probably have smaller sizes:
resize!(GLOBAL_TREEBRAIDER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)
resize!(GLOBAL_TREETRANSPOSER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)
resize!(GLOBAL_DIAGONAL_TREEBRAIDER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)
resize!(GLOBAL_DIAGONAL_TREETRANSPOSER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)
resize!(GLOBAL_DIAGONAL_TREETWISTER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)
resize!(GLOBAL_TREETRACER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)
resize!(GLOBAL_TREETWISTER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)

return nothing
end

# Allocations must outlive any GPUArrays allocation-cache scope around the operation.
device_transformer(A, transformer::UniqueTreeTransformer, p) =
device_transformer(A, DeviceUniqueTreeTransformer(transformer, p))
Expand Down Expand Up @@ -690,23 +677,24 @@ function diagonal_device_transformer(A, transformer, dstdiagonal::Bool)
return device_transformer(A, DeviceUniqueTreeTransformer(blocks, work_offsets, nwork))
end

@cached function diagonal_treebraider(
# Transformers for diagonal sources share the cache of the other device transformers.
@cached function TensorKit.device_treebraider(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, levels::IndexTuple, dstdiagonal::Bool
) where {TA <: AnyGPUVector}
host = TensorKit.treebraider(Vector{scalartype(A)}, Vdst, Vsrc, p, conjsrc, levels)
return diagonal_device_transformer(A, host, dstdiagonal)
end

@cached function diagonal_treetransposer(
@cached function TensorKit.device_treetransposer(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, dstdiagonal::Bool
) where {TA <: AnyGPUVector}
host = TensorKit.treetransposer(Vector{scalartype(A)}, Vdst, Vsrc, p, conjsrc)
return diagonal_device_transformer(A, host, dstdiagonal)
end

@cached function diagonal_treetwister(
@cached function TensorKit.device_treetwister(
A::Type{TA}, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple,
conjsrc::Bool, twistmask::NTuple{N, Bool}, inv::Bool, dstdiagonal::Bool
) where {TA <: AnyGPUVector, N}
Expand All @@ -731,7 +719,7 @@ Base.@propagate_inbounds function TensorKit._braid!(
)
Vdst, Vsrc = space(tdst), space(tsrc)
@boundscheck TensorKit.spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc)
transformer = diagonal_treebraider(
transformer = TensorKit.device_treebraider(
storagetype(tdst), Vdst, Vsrc, p, conjsrc, levels, tdst isa DiagonalTensorMap
)
return _add_diagonal!(tdst, tsrc, conjsrc, transformer, α, β)
Expand All @@ -743,7 +731,7 @@ Base.@propagate_inbounds function TensorKit._transpose!(
)
Vdst, Vsrc = space(tdst), space(tsrc)
@boundscheck TensorKit.spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc)
transformer = diagonal_treetransposer(
transformer = TensorKit.device_treetransposer(
storagetype(tdst), Vdst, Vsrc, p, conjsrc, tdst isa DiagonalTensorMap
)
return _add_diagonal!(tdst, tsrc, conjsrc, transformer, α, β)
Expand All @@ -755,7 +743,7 @@ Base.@propagate_inbounds function TensorKit._twist!(
)
Vdst, Vsrc = space(tdst), space(tsrc)
@boundscheck TensorKit.spacecheck_transform(permute, Vdst, Vsrc, p, conjsrc)
transformer = diagonal_treetwister(
transformer = TensorKit.device_treetwister(
storagetype(tdst), Vdst, Vsrc, p, conjsrc, twistmask, inv, tdst isa DiagonalTensorMap
)
return _add_diagonal!(tdst, tsrc, conjsrc, transformer, α, β)
Expand Down
1 change: 0 additions & 1 deletion src/TensorKit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ const TO = TensorOperations
using MatrixAlgebraKit

using Dictionaries: Dictionaries, Dictionary, Indices, gettoken, gettokenvalue
using LRUCache
using OhMyThreads
using ScopedValues
using TimerOutputs: TimerOutputs, TimerOutput, @timeit_debug
Expand Down
Loading
Loading