Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/src/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ When releasing a new version, move the "Unreleased" changes to a new version sec
### Added

- Re-export `NamedSector` and `@NamedSector` from TensorKitSectors
- `twist(src, inds; inv = false)` for fusion tree pairs and blocks, returning `src` together with the scalar twist factor of the legs `inds`, which can be given as indices or as a `Bool` mask
- `twist!(tdst, tsrc, inds, α = 1, β = 0, [backend], [allocator]; inv = false)` computing `tdst = β * tdst + α * twist(tsrc, inds; inv)`, in line with `permute!` and `braid!`, together with Mooncake and Enzyme rules; `twist` now also accepts `backend` and `allocator` keywords

### Changed

Expand All @@ -44,6 +46,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec
- Recoupling matrices are stored such that they can leverage BLAS, also for mixed complex tensor with real recoupling coefficients.
- Constructing a `FusionTreeBlock` only takes the fusion product of the shorter side, and `fusionblocks` groups the cached fusion trees of the space instead of constructing a block for every combination of sectors (internal)
- Fusion tree block manipulations of (named) product sectors are computed as Kronecker products of the manipulations of their parts, splitting off the `UniqueFusion` sectors, such that they share the caches of the individual sectors ([#592](https://github.com/QuantumKitHub/TensorKit.jl/pull/592))
- `twist!` and `twist` on a `TensorMap` or its adjoint use a cached tree transformer and the index manipulation kernel instead of a loop over all fusion trees, which removes the per-subblock bookkeeping and turns a twist of a GPU tensor into a single kernel launch ([#595](https://github.com/QuantumKitHub/TensorKit.jl/issues/595))

## [0.17.2](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.1...v0.17.2) - 2026-09-20

Expand Down
1 change: 1 addition & 0 deletions docs/src/lib/fusiontrees.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ repartition(src::Union{FusionTreePair, FusionTreeBlock}, N::Int)
Base.transpose(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple)
braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, levels::Index2Tuple)
permute(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple)
twist(src::Union{FusionTreePair, FusionTreeBlock}, inds)
```
138 changes: 138 additions & 0 deletions ext/TensorKitEnzymeExt/indexmanipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,144 @@ function EnzymeRules.forward(
end
end

function EnzymeRules.augmented_primal(
config::EnzymeRules.RevConfigWidth{1},
func::Const{typeof(TK.twist!)},
::Type{RT},
C::Annotation{<:AbstractTensorMap},
A::Annotation{<:AbstractTensorMap},
inds::Const,
α::Annotation{<:Number},
β::Annotation{<:Number},
ba::Const...;
inv::Bool = false
) where {RT}
C_cache = !isa(β, Const) ? copy(C.val) : nothing
A_cache = EnzymeRules.overwritten(config)[3] ? copy(A.val) : nothing
# if we need to compute Δa, it is faster to allocate an intermediate twisted A
# and store that instead of repeating the twist in the pullback each time.
# effectively, we replace `twist!` by `add ∘ twist`.
Ap = if !isa(α, Const)
Ap = twist(A.val, inds.val; inv, copy = true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to permute and transform, is there any chance this could secretly be aliasing A.val?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what the copy=true is for right?

add!(C.val, Ap, α.val, β.val)
Ap
else
bavs = map(a -> a.val, ba)
TK.twist!(C.val, A.val, inds.val, α.val, β.val, bavs...; inv)
nothing
end
cache = (C_cache, A_cache, Ap)
primal = EnzymeRules.needs_primal(config) ? C.val : nothing
shadow = EnzymeRules.needs_shadow(config) ? C.dval : nothing
return EnzymeRules.AugmentedReturn(primal, shadow, cache)
end
function EnzymeRules.reverse(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was Enzyme not able to make its own rules for these? Kind of sad if so...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes make twist go through the same path as permute and transpose etc, so even if it was able to do this I think this path is way more friendly on both compile time and possibly runtime?

config::EnzymeRules.RevConfigWidth{1},
func::Const{typeof(TK.twist!)},
::Type{RT},
cache,
C::Annotation{<:AbstractTensorMap},
A::Annotation{<:AbstractTensorMap},
inds::Const,
α::Annotation{<:Number},
β::Annotation{<:Number},
ba::Const...;
inv::Bool = false
) where {RT}
C_cache, A_cache, Ap = cache
Cval = something(C_cache, C.val)
Aval = something(A_cache, A.val)
bavs = map(a -> a.val, ba)
# ΔA
if !isa(A, Const) && !isa(C, Const)
TK.twist_pb(A.dval, Aval, C.dval, C.val, inds.val, α.val, bavs...; inv)
end
Δαr = pullback_dα(α, C, Ap)
Δβr = pullback_dβ(β, C, Cval)
!isa(C, Const) && pullback_dC!(C.dval, β.val)
return nothing, nothing, nothing, Δαr, Δβr, map(Returns(nothing), ba)...
end
function EnzymeRules.forward(
config::EnzymeRules.FwdConfigWidth{1},
func::Const{typeof(TK.twist!)},
::Type{RT},
C::Annotation{<:AbstractTensorMap},
A::Annotation{<:AbstractTensorMap},
inds::Annotation,
α::Annotation{<:Number},
β::Annotation{<:Number},
ba::Const...;
inv::Bool = false
) where {RT}
bavs = map(a -> a.val, ba)
if !isa(C, Const)
if isa(β, Const)
scale!(C.dval, β.val)
else
add!(C.dval, C.val, β.dval, β.val)
end
!isa(α, Const) && TK.twist!(C.dval, A.val, inds.val, α.dval, One(), bavs...; inv)
!isa(A, Const) && TK.twist!(C.dval, A.dval, inds.val, α.val, One(), bavs...; inv)
end
TK.twist!(C.val, A.val, inds.val, α.val, β.val, bavs...; inv)
if EnzymeRules.needs_primal(config) && EnzymeRules.needs_shadow(config)
return C
elseif EnzymeRules.needs_primal(config)
return C.val
elseif EnzymeRules.needs_shadow(config)
return C.dval
else
return nothing
end
end

# in-place twist: the source and destination are the same tensor
function EnzymeRules.augmented_primal(
config::EnzymeRules.RevConfigWidth{1},
func::Const{typeof(TK.twist!)},
::Type{RT},
t::Annotation{<:AbstractTensorMap},
inds::Const;
inv::Bool = false
) where {RT}
TK.twist!(t.val, inds.val; inv)
primal = EnzymeRules.needs_primal(config) ? t.val : nothing
shadow = EnzymeRules.needs_shadow(config) ? t.dval : nothing
return EnzymeRules.AugmentedReturn(primal, shadow, nothing)
end
function EnzymeRules.reverse(
config::EnzymeRules.RevConfigWidth{1},
func::Const{typeof(TK.twist!)},
::Type{RT},
cache,
t::Annotation{<:AbstractTensorMap},
inds::Const;
inv::Bool = false
) where {RT}
!isa(t, Const) && TK.twist!(t.dval, inds.val; inv = !inv)
return (nothing, nothing)
end
function EnzymeRules.forward(
config::EnzymeRules.FwdConfigWidth{1},
func::Const{typeof(TK.twist!)},
::Type{RT},
t::Annotation{<:AbstractTensorMap},
inds::Annotation;
inv::Bool = false
) where {RT}
TK.twist!(t.val, inds.val; inv)
!isa(t, Const) && TK.twist!(t.dval, inds.val; inv)
if EnzymeRules.needs_primal(config) && EnzymeRules.needs_shadow(config)
return t
elseif EnzymeRules.needs_primal(config)
return t.val
elseif EnzymeRules.needs_shadow(config)
return t.dval
else
return nothing
end
end

function EnzymeRules.augmented_primal(
config::EnzymeRules.RevConfigWidth{1},
func::Const{typeof(flip)},
Expand Down
1 change: 1 addition & 0 deletions ext/TensorKitEnzymeExt/utility.jl
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ end
@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.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
10 changes: 10 additions & 0 deletions ext/TensorKitGPUArraysExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,14 @@ end
return device_transformer(A, host, linearize(p))
end

@cached function TensorKit.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__()
Expand All @@ -295,12 +303,14 @@ function __init__()
TensorKit.GLOBAL_CACHES, (
Symbol("TensorKitGPUArraysExt.GLOBAL_TREEBRAIDER_CACHE") => GLOBAL_TREEBRAIDER_CACHE,
Symbol("TensorKitGPUArraysExt.GLOBAL_TREETRANSPOSER_CACHE") => GLOBAL_TREETRANSPOSER_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_TREETWISTER_CACHE; maxsize = DEFAULT_DEVICECACHE_SIZE)

return nothing
end
Expand Down
87 changes: 87 additions & 0 deletions ext/TensorKitMooncakeExt/indexmanipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,93 @@ function Mooncake.rrule!!(
return t_Δt, twist_pullback
end

# both are needed for correctly capturing every dispatch
@is_primitive(
DefaultCtx,
ReverseMode,
Tuple{
typeof(TK.twist!),
AbstractTensorMap,
AbstractTensorMap, Any,
Number, Number, Vararg{Any},
}
)
@is_primitive(
DefaultCtx,
ReverseMode,
Tuple{
typeof(Core.kwcall), @NamedTuple{inv::Bool}, typeof(TK.twist!),
AbstractTensorMap,
AbstractTensorMap, Any,
Vararg{Any},
}
)

function Mooncake.rrule!!(
::CoDual{typeof(TK.twist!)},
C_ΔC::CoDual{<:AbstractTensorMap},
A_ΔA::CoDual{<:AbstractTensorMap}, inds_Δinds::CoDual,
α_Δα::CoDual{<:Number}, β_Δβ::CoDual{<:Number},
ba_Δba::CoDual...
)
C_ΔC, pullback = twist!_rrule(C_ΔC, A_ΔA, inds_Δinds, false, (α_Δα, β_Δβ, ba_Δba...))
twist!_pullback(Δ::NoRData) = (NoRData(), pullback(Δ)...)
return C_ΔC, twist!_pullback
end
function Mooncake.rrule!!(
::CoDual{typeof(Core.kwcall)}, kwargs_Δkwargs::CoDual{@NamedTuple{inv::Bool}}, ::CoDual{typeof(TK.twist!)},
C_ΔC::CoDual{<:AbstractTensorMap},
A_ΔA::CoDual{<:AbstractTensorMap}, inds_Δinds::CoDual,
args_Δargs::CoDual...
)
inv = primal(kwargs_Δkwargs).inv
C_ΔC, pullback = twist!_rrule(C_ΔC, A_ΔA, inds_Δinds, inv, args_Δargs)
twist!_pullback(Δ::NoRData) = (NoRData(), NoRData(), NoRData(), pullback(Δ)...)
return C_ΔC, twist!_pullback
end

# `args_Δargs` holds the optional `α`, `β`, backend and allocator, where a missing `α` or `β` is constant
function twist!_rrule(C_ΔC, A_ΔA, inds_Δinds, inv, args_Δargs)
# prepare arguments
C, ΔC = arrayify(C_ΔC)
A, ΔA = arrayify(A_ΔA)
inds = primal(inds_Δinds)
args = primal.(args_Δargs)
α = length(args) >= 1 ? args[1] : One()
β = length(args) >= 2 ? args[2] : Zero()
ba = length(args) >= 3 ? args[3:end] : ()

C_cache = copy(C)

# if we need to compute Δa, it is faster to allocate an intermediate twisted A
# and store that instead of repeating the twist in the pullback each time.
# effectively, we replace `twist!` by `add ∘ twist`.
Ap = if _needs_tangent(α)
Ap = twist(A, inds; inv, copy = true)
add!(C, Ap, α, β)
Ap
else
TK.twist!(C, A, inds, α, β, ba...; inv)
nothing
end

function pullback(::NoRData)
copy!(C, C_cache)

TK.twist_pb(ΔA, A, ΔC, C, inds, α, ba...; inv)
ΔAr = NoRData()

Δαr = length(args) >= 1 ? pullback_dα(α, ΔC, Ap) : nothing
Δβr = length(args) >= 2 ? pullback_dβ(β, ΔC, C) : nothing
ΔCr = pullback_dC!(ΔC, β) # this typically returns NoRData()

Δargsr = (Δαr, Δβr, map(Returns(NoRData()), ba)...)[1:length(args)]
return ΔCr, ΔAr, NoRData(), Δargsr...
end

return C_ΔC, pullback
end

# both are needed for correctly capturing every dispatch
@is_primitive DefaultCtx ReverseMode Tuple{typeof(flip), AbstractTensorMap, Any}
@is_primitive DefaultCtx ReverseMode Tuple{typeof(Core.kwcall), @NamedTuple{inv::Bool}, typeof(flip), AbstractTensorMap, Any}
Expand Down
2 changes: 1 addition & 1 deletion src/auxiliary/caches.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ end

# category of the miss-path (construction) timer section of an `@cached` function
function _cached_category(fname::Symbol)
return fname in (:fsbraid, :fstranspose, :treebraider, :treetransposer) ?
return fname in (:fsbraid, :fstranspose, :treebraider, :treetransposer, :treetwister) ?
"symmetry" : "bookkeeping"
end

Expand Down
17 changes: 17 additions & 0 deletions src/fusiontrees/braiding_manipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,23 @@ function permute(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple)
return braid(src, p, (levels1, levels2))
end

"""
twist(src::Union{FusionTreePair, FusionTreeBlock}, inds; inv::Bool = false) -> (src, θ)

Apply a twist to the uncoupled legs `inds` of the fusion-splitting tree pair or block `src`,
where `inds` is either a collection of leg indices or an `NTuple{N, Bool}` mask over all legs.
The twist is diagonal in the fusion tree basis, so this returns `src` itself together with the
scalar factor `θ`, which is shared by all trees in a block. If `inv = true`, use the inverse twist.
"""
function twist(src::Union{FusionTreePair{I}, FusionTreeBlock{I}}, inds; inv::Bool = false) where {I}
uncoupled₁, uncoupled₂ = src isa FusionTreeBlock ? src.uncoupled : (src[1].uncoupled, src[2].uncoupled)
uncoupled = (uncoupled₁..., uncoupled₂...)
N = length(uncoupled)
mask = inds isa NTuple{N, Bool} ? inds : ntuple(i -> i in inds, N)
θ = prod(map((m, a) -> m ? twist(a) : one(twist(a)), mask, uncoupled); init = one(sectorscalartype(I)))
return src, inv ? θ' : θ
end

"""
flip((f₁, f₂)::FusionTreePair, i::Int; inv::Bool = false)
-> SingletonDict{FusionTreePair, <:Number}
Expand Down
11 changes: 11 additions & 0 deletions src/pullbacks/indexmanipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ for transform in (:permute, :transpose)
return
end
end
function twist_pb(ΔA, A, ΔC, C, inds, α, ba...; inv::Bool = false)
TC = promote_twist(VectorInterface.promote_scale(ΔC, α), sectortype(ΔC))
if scalartype(ΔA) <: Real && !(TC <: Real)
ΔAc = similar(ΔC, TC)
twist!(ΔAc, ΔC, inds, conj(α), Zero(), ba...; inv = !inv)
add!(ΔA, real(ΔAc))
else
twist!(ΔA, ΔC, inds, conj(α), One(), ba...; inv = !inv)
end
return
end
function braid_pb(ΔA, A, ΔC, C, p, levels, α, ba...)
ip = invperm(linearize(p))
pΔA = TO.repartition(ip, numout(A))
Expand Down
Loading
Loading