Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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.207"
EnzymeTestUtils = "0.2.8"
FiniteDifferences = "0.12"
GPUArrays = "11.4.1"
LRUCache = "1.6"
LinearAlgebra = "1"
MatrixAlgebraKit = "0.6.9"
MemoizationKit = "0.1"
Mooncake = "0.5.27"
OhMyThreads = "0.8.0"
PrecompileTools = "1"
Expand Down
3 changes: 2 additions & 1 deletion docs/src/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ When releasing a new version, move the "Unreleased" changes to a new version sec

- Extend VectorInterface compat to include v0.7
- Require TensorKitSectors v0.3.10
- `@cached` can be used from other modules, such as package extensions, which then own the cache of the methods they add; these caches are shown with their module in `global_cache_info` after registration in `__init__` (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
- Storage-specific transformers of package extensions are cached by adding `@cached` methods to `device_treebraider` and `device_treetransposer`, to which `treebraider` and `treetransposer` forward, such that their caches can be sized independently (internal)
- The `TreeTransformer`s used in index manipulations are now constructed and cached per storagetype of the destination tensor, which is also a dispatch point for storage-specific transformers with their own cache (internal)

### Deprecated
Expand Down
24 changes: 23 additions & 1 deletion docs/src/man/fusiontrees.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,29 @@ 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/).

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
10 changes: 5 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,13 +142,13 @@ 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.treetransposer), ::Any...) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.treebraider), ::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.insertleftunit), ::HomSpace, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.insertrightunit), ::HomSpace, ::Any) = nothing
@inline EnzymeRules.inactive(::typeof(TensorKit.removeunit), ::HomSpace, ::Any) = nothing
Expand Down
31 changes: 11 additions & 20 deletions ext/TensorKitGPUArraysExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ end

# strides of a dense array of shape `sz`
_dense_strides(size::Dims) = (1, Base.front(cumprod(size))...)
_dense_strides(::Tuple{}) = ()

# `permute(Vsrc, p) == Vdst` is enforced when the transformer is built, so the permuted
# source shape always matches `sz_dst` and the two views share Cartesian inds.
Expand Down Expand Up @@ -271,40 +272,30 @@ function DeviceGenericTreeTransformer(
)
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)

# 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

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,
)
)

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

return nothing
end

# Allocations must outlive any GPUArrays allocation-cache scope around the operation.
function device_transformer(A, transformer::UniqueTreeTransformer, p)
packed = DeviceUniqueTreeTransformer(transformer, p)
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