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
24 changes: 24 additions & 0 deletions src/spaces/homspace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ See also [`degeneracystructure`](@ref), [`blockstructure`](@ref).
"""
subblockstructure(W::HomSpace) = Dictionary(fusiontrees(W), degeneracystructure(W).subblockstructure)

"""
adjoint_subblockstructure(W::HomSpace) -> Dictionary

Subblock structure of `W'`, expressed as strides into a buffer laid out for `W`.

This mirrors the relation used by `subblock(::AdjointTensorMap, ...)`: the adjoint shares its
parent's data, with the tree pair swapped and sizes and strides permuted by
`(domainind..., codomainind...)`. Permuting `adjoint(t)` can therefore read `t`'s own buffer
directly, instead of materialising an `AdjointTensorMap` and falling back to the uncached
tree-transformation path.

See also [`subblockstructure`](@ref).
"""
function adjoint_subblockstructure(W::HomSpace)

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.

code-organization wise, would it be easier to make this subblockstructure(W::HomSpace, conjW::Bool=false)?

N₁, N₂ = numout(W), numin(W)
swap = (ntuple(i -> N₁ + i, N₂)..., ntuple(identity, N₁)...)
structs = subblockstructure(W)
newkeys = map(((f₁, f₂),) -> (f₂, f₁), collect(keys(structs)))
newvals = map(collect(values(structs))) do (sz, str, off)
Comment on lines +189 to +190

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.

it feels like it should be possible to avoid the collect calls here, but I'd have to dig through the Dictionaries interface(s) to see what is possible?

return (TupleTools.getindices(sz, swap), TupleTools.getindices(str, swap), off)
end
return Dictionary(newkeys, newvals)
end

"""
fusionblocks(W::HomSpace)

Expand Down
40 changes: 32 additions & 8 deletions src/tensors/indexmanipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,29 @@ Base.@deprecate(
return tdst
end

"""
add_conj_transform!(tdst, tsrc, p, transformer, α, β, backend, allocator) -> tdst

Compute `tdst = β * tdst + α * permute(adjoint(tsrc), p)` while reading `tsrc`'s own data
buffer, without materialising an `AdjointTensorMap`.

`transformer` must come from [`conj_treebraider`](@ref), whose source strides address the
parent's storage; the conjugation itself is folded into the strided reads.
"""
@propagate_inbounds function add_conj_transform!(
tdst::TensorMap, tsrc::TensorMap, p::Index2Tuple, transformer,
α::Number, β::Number, backend, allocator
)
@boundscheck spacecheck_transform(permute, tdst, tsrc', p)
ntasks = use_threaded_transform(tdst, transformer) ? get_num_transformer_threads() : 1
scheduler = ntasks == 1 ? SerialScheduler() : DynamicScheduler(; ntasks, split = :roundrobin)
add_transform_kernel!(
tdst.data, tsrc.data, p, transformer, α, β, backend, allocator, scheduler;
conjsrc = true
)
return tdst
end

function use_threaded_transform(t::TensorMap, transformer)
return get_num_transformer_threads() > 1 && length(t.data) > Strided.MINTHREADLENGTH
end
Expand All @@ -587,14 +610,15 @@ function use_threaded_transform(t::AbstractTensorMap, transformer)
end

function add_transform_kernel!(
tdst, tsrc, p, transformer, α, β, backend, allocator, scheduler
tdst, tsrc, p, transformer, α, β, backend, allocator, scheduler;
conjsrc::Bool = false
Comment on lines +613 to +614

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.

Suggested change
tdst, tsrc, p, transformer, α, β, backend, allocator, scheduler;
conjsrc::Bool = false
tdst, tsrc, p, conjsrc, transformer, α, β, backend, allocator, scheduler

little bit of a nitpick, but I'd be inclined to just make this a mandatory (positional) argument instead, which is more in line with the remainder of these functions. Obviously doens't matter, just for consistency.

)
I = sectortype(tdst)
if FusionStyle(I) === UniqueFusion()
tforeach(fusiontrees(tsrc); scheduler) do (f₁, f₂)
(f₁′, f₂′), coeff = transformer((f₁, f₂))
@inbounds TO.tensoradd!(
tdst[f₁′, f₂′], tsrc[f₁, f₂], p, false, α * coeff, β, backend, allocator
tdst[f₁′, f₂′], tsrc[f₁, f₂], p, conjsrc, α * coeff, β, backend, allocator
)
end
return nothing
Expand Down Expand Up @@ -629,7 +653,7 @@ function add_transform_kernel!(
@inbounds for (i, (f₁, f₂)) in enumerate(fusiontrees(src))
TO.tensoradd!(
sreshape(view(buffer_src, :, i), sz_src), tsrc[f₁, f₂],
ptriv, false, One(), Zero(), backend, allocator
ptriv, conjsrc, One(), Zero(), backend, allocator
)
end

Expand Down Expand Up @@ -657,19 +681,19 @@ end
# repeated specialization -- this only depends on `numind` and `eltype`.
function add_transform_kernel!(
data_dst::DenseVector, data_src::DenseVector, p, transformer::AbelianTreeTransformer,
α, β, backend, allocator, scheduler
α, β, backend, allocator, scheduler; conjsrc::Bool = false
)
tforeach(transformer.data; scheduler) do (coeff, struct_dst, struct_src)
TO.tensoradd!(
StridedView(data_dst, struct_dst...), StridedView(data_src, struct_src...),
p, false, α * coeff, β, backend, allocator
p, conjsrc, α * coeff, β, backend, allocator
)
end
return nothing
end
function add_transform_kernel!(
data_dst::DenseVector, data_src::DenseVector, p, transformer::GenericTreeTransformer,
α, β, backend, allocator, scheduler
α, β, backend, allocator, scheduler; conjsrc::Bool = false
)
cp = TO.allocator_checkpoint!(allocator)

Expand All @@ -686,7 +710,7 @@ function add_transform_kernel!(
TO.tensoradd!(
StridedView(data_dst, sz_dst, only(structs_dst)...),
StridedView(data_src, sz_src, only(structs_src)...),
p, false, α * coeff, β, backend, allocator
p, conjsrc, α * coeff, β, backend, allocator
)
else # Multi-tree block: pack → recoupling matmul → unpack.
rows, cols = size(U)
Expand All @@ -701,7 +725,7 @@ function add_transform_kernel!(
@inbounds for (i, struct_src_i) in enumerate(structs_src)
TO.tensoradd!(
sreshape(view(buffer_src, :, i), sz_src), StridedView(data_src, sz_src, struct_src_i...),
ptriv, false, One(), Zero(), backend, allocator
ptriv, conjsrc, One(), Zero(), backend, allocator
)
end

Expand Down
18 changes: 16 additions & 2 deletions src/tensors/tensoroperations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,23 @@ function TO.tensoradd!(
return C
end
if conjA
A′ = adjoint(A)
pA′ = adjointtensorindices(A, _canonicalize(pA, C))
permute!(C, A′, pA′, α, β, backend, allocator)
if C isa TensorMap && A isa TensorMap

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.

It somehow feels like this specialization might make more sense on the permute!-level directly, where we could intercept TensorMap-AdjointTensorMap combinations directly? That would also immediately catch manual calls to permute!(tdst, tsrc', ...) in the same way.

To achieve this, I'm wondering if it makes sense to just have treebraider(Vdst, Vsrc, p, levels, conj) as the entrypoint, which then also fixes the caching keys being distinct? There's some subtleties left with this, mostly with respect to other combinations like permute!(tdst', tsrc, ...), which we would then probably want to map to the other case, but that might be reasonable?

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.

I think the first part of this comment is more similar to what is sketched in #519, as opposed to the tensoradd!-level fix that is sketched here.

# Both operands have a flat data buffer, so the adjoint never has to be
# materialised: `conj_treebraider` yields a memoized transformer whose source
# strides address `A`'s own storage. Materialising `adjoint(A)` here would push
# a perfectly ordinary TensorMap onto the uncached tree-transformation path.
n₁ = numin(A) # == numout(adjoint(A))
levels = ntuple(identity, numind(A))
levels′ = (
TupleTools.getindices(levels, ntuple(identity, n₁)),
TupleTools.getindices(levels, n₁ .+ ntuple(identity, numout(A))),
)
transformer = conj_treebraider(space(C), space(A), pA′, levels′)
@inbounds add_conj_transform!(C, A, pA′, transformer, α, β, backend, allocator)
else
permute!(C, adjoint(A), pA′, α, β, backend, allocator)
end
else
permute!(C, A, _canonicalize(pA, C), α, β, backend, allocator)
end
Expand Down
29 changes: 22 additions & 7 deletions src/tensors/treetransformers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ struct AbelianTreeTransformer{T, N} <: TreeTransformer
data::Vector{AbelianTransformerData{T, N}}
end

function AbelianTreeTransformer(transform, p, Vdst, Vsrc)
function AbelianTreeTransformer(transform, p, Vdst, Vsrc; srcstructure = subblockstructure(Vsrc))
t₀ = Base.time()
permute(Vsrc, p) == Vdst || throw(SpaceMismatch("Incompatible spaces for permuting."))
fts_src = subblockstructure(Vsrc)
fts_src = srcstructure
fts_dst = subblockstructure(Vdst)
L = length(fts_src)
T = sectorscalartype(sectortype(Vdst))
Expand Down Expand Up @@ -51,11 +51,11 @@ struct GenericTreeTransformer{T, N} <: TreeTransformer
data::Vector{GenericTransformerData{T, N}}
end

function GenericTreeTransformer(transform, p, Vdst, Vsrc)
function GenericTreeTransformer(transform, p, Vdst, Vsrc; srcstructure = subblockstructure(Vsrc))
t₀ = Base.time()
permute(Vsrc, p) == Vdst || throw(SpaceMismatch("Incompatible spaces for permuting."))
fusionstructure_dst = subblockstructure(Vdst)
fusionstructure_src = subblockstructure(Vsrc)
fusionstructure_src = srcstructure
I = sectortype(Vsrc)
T = sectorscalartype(I)
N = numind(Vdst)
Expand Down Expand Up @@ -139,7 +139,8 @@ function treetransformertype(Vdst, Vsrc)
end

function TreeTransformer(
transform::Function, p, Vdst::HomSpace{S}, Vsrc::HomSpace{S}
transform::Function, p, Vdst::HomSpace{S}, Vsrc::HomSpace{S};
srcstructure = subblockstructure(Vsrc)
) where {S}
permute(Vsrc, p) == Vdst ||
throw(SpaceMismatch("Incompatible spaces for permuting"))
Expand All @@ -148,8 +149,8 @@ function TreeTransformer(
I === Trivial && return TrivialTreeTransformer()

return FusionStyle(I) == UniqueFusion() ?
AbelianTreeTransformer(transform, p, Vdst, Vsrc) :
GenericTreeTransformer(transform, p, Vdst, Vsrc)
AbelianTreeTransformer(transform, p, Vdst, Vsrc; srcstructure) :
GenericTreeTransformer(transform, p, Vdst, Vsrc; srcstructure)
end

# braid is special because it has levels
Expand All @@ -166,6 +167,20 @@ end
return TreeTransformer(fusiontreebraider, p, Vdst, Vsrc)
end

# Transformer for braiding `adjoint(t)` while reading `t`'s own data buffer: the tree
# transformation is that of `Vparent'`, but the source strides address the parent's storage
# (see `adjoint_subblockstructure`). Kept as a separate cached function so that its entries
# cannot collide with the plain `treebraider` ones for the same pair of spaces.
@cached function conj_treebraider(
Vdst::TensorMapSpace, Vparent::TensorMapSpace, p::Index2Tuple, levels
)::treetransformertype(Vdst, Vparent')
fusiontreebraider(f) = braid(f, p, levels)
return TreeTransformer(
fusiontreebraider, p, Vdst, Vparent';
srcstructure = adjoint_subblockstructure(Vparent)
)
end

function treetransposer(::AbstractTensorMap, ::AbstractTensorMap, p::Index2Tuple)
return fusiontreetransform(f) = transpose(f, p)
end
Expand Down
Loading