Skip to content

Rework index manipulation API#416

Open
lkdvos wants to merge 33 commits into
mainfrom
ld-indexmanipulations
Open

Rework index manipulation API#416
lkdvos wants to merge 33 commits into
mainfrom
ld-indexmanipulations

Conversation

@lkdvos
Copy link
Copy Markdown
Member

@lkdvos lkdvos commented Apr 26, 2026

Summary

This PR overhauls the index manipulation API in src/tensors/indexmanipulations.jl to match TensorOperations dispatch conventions, reduces code duplication in the implementation, and adds a dedicated documentation page.
The goal was a bunch of code simplification, (overall number of lines reduced, even though I added some docs 🎉 )

API changes

  • Unified in-place interface: permute!, braid!, transpose!, and repartition! now directly accept α, β, backend, and allocator as optional arguments (with defaults One(), Zero(), DefaultBackend(), DefaultAllocator()), following the TensorOperations dispatch pattern. The old add_permute!, add_braid!, and add_transpose! are deprecated and forward to the new functions.
  • allocator support: previously, the index manipulation functions did not support a custom allocator at all. It is now a positional argument in both the public and internal interfaces, consistent with TensorOperations convention.
  • Out-of-place functions (permute, braid, transpose, repartition) gain backend as a new keyword argument alongside the now-supported allocator keyword.

Implementation changes

  • All permutation operations now route through braid!, eliminating duplicate codepaths.
  • Dedicated braid! method added for AdjointTensorMap.
  • add_transform! kernels for TensorMap refactored to operate on the raw data vector rather than the full TensorMap. Because the data vector has no symmetry type, this avoids recompilation for every TensorMap type combination, improving compilation time.
  • Various minor simplifications throughout.

@github-actions
Copy link
Copy Markdown
Contributor

After the build completes, the updated documentation will be available here

@lkdvos lkdvos force-pushed the ld-indexmanipulations branch from f45ab42 to 2841304 Compare April 26, 2026 12:57
@lkdvos
Copy link
Copy Markdown
Member Author

lkdvos commented Apr 26, 2026

This might also resolve #413, where I tried to compute cond with eigh_vals instead of LinearAlgebra.eigvals.

@lkdvos lkdvos force-pushed the ld-indexmanipulations branch 3 times, most recently from ff15f1e to c4408b3 Compare April 26, 2026 21:33
@lkdvos lkdvos marked this pull request as ready for review April 26, 2026 22:02
@lkdvos lkdvos requested review from Jutho and kshyatt April 26, 2026 22:03
@lkdvos lkdvos linked an issue Apr 26, 2026 that may be closed by this pull request
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 89.83957% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/tensors/indexmanipulations.jl 89.44% 17 Missing ⚠️
ext/TensorKitMooncakeExt/indexmanipulations.jl 90.00% 1 Missing ⚠️
src/tensors/tensoroperations.jl 50.00% 1 Missing ⚠️
Files with missing lines Coverage Δ
ext/TensorKitAMDGPUExt/roctensormap.jl 52.11% <100.00%> (+0.68%) ⬆️
ext/TensorKitCUDAExt/cutensormap.jl 74.32% <100.00%> (-0.35%) ⬇️
src/TensorKit.jl 13.79% <ø> (ø)
src/planar/planaroperations.jl 72.79% <100.00%> (ø)
src/tensors/braidingtensor.jl 70.39% <100.00%> (ø)
src/tensors/treetransformers.jl 83.16% <100.00%> (+0.44%) ⬆️
ext/TensorKitMooncakeExt/indexmanipulations.jl 96.11% <90.00%> (ø)
src/tensors/tensoroperations.jl 93.95% <50.00%> (ø)
src/tensors/indexmanipulations.jl 90.04% <89.44%> (+16.10%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lkdvos lkdvos force-pushed the ld-indexmanipulations branch 3 times, most recently from a0bc84b to 2c44dca Compare April 28, 2026 20:41
@lkdvos lkdvos force-pushed the ld-indexmanipulations branch 2 times, most recently from b833340 to 5a576f0 Compare May 12, 2026 12:46
Comment thread src/tensors/braidingtensor.jl Outdated
Comment thread src/tensors/indexmanipulations.jl Outdated
Comment thread src/tensors/indexmanipulations.jl Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/indexmanipulations.md Outdated
Comment thread docs/src/man/linearalgebra.md Outdated
Comment thread docs/src/man/contractions.md Outdated
@lkdvos lkdvos force-pushed the ld-indexmanipulations branch from cae3748 to 26c844a Compare May 19, 2026 20:12
@lkdvos
Copy link
Copy Markdown
Member Author

lkdvos commented May 19, 2026

I've separated out the docs changes in an attempt to not conflate the two distinct changes in this PR. I think right now everything here should pass, so it should be ready for a round of review, before it can be merged.
The associated docs changes will then be for a separate PR.

Comment thread src/tensors/indexmanipulations.jl
Comment on lines +343 to 352
function braid(
t::AdjointTensorMap, (p₁, p₂)::Index2Tuple, levels::IndexTuple;
kwargs...
)
p₁′ = adjointtensorindices(t, p₂)
p₂′ = adjointtensorindices(t, p₁)
perm = adjointtensorindices(adjoint(t), ntuple(identity, numind(t)))
levels′ = TupleTools.getindices(levels, perm)
return adjoint(braid(adjoint(t), (p₁′, p₂′), levels′; kwargs...))
end
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.

Is this a completely new definition. I am wondering about its correctness, in particular with respect to the definition of levels′. Given that the adjoint of an overbraid is an underbraid, it might be that we want to change levels to map(-, levels), in combination with applying the permuation perm?

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.

Ok in some simple example that I tested it with, the current implementation is correct, so never mind.

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 did end up adding some test for this since indeed it is a new implementation, I think I've convinced myself that while indeed you want underbraids on the adjoint, this ends up being the case because the index order gets reversed

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.

What index order? I am still confused. The index mappings are such that you do the corresponding braid on the adjoint tensor? In what sense is that reversing index order?

As a side note (since I like derailing PRs 😄 ), one thing I noticed in trying to test this, is that copy(::AdjointTensor) produces a new ::AdjointTensor. This is different from Matrix, where copy of an adjoint produces a regular Matrix. Also TensorMap(::AdjointTensorMap) doesn't work, so it took me a while to find a good way to reinstantiate an AdjointTensorMap as its corresponding TensorMap.

end
# 2. Recoupling: buffer_dst = buffer_src * U^T (each output tree is a linear
# combination of input trees weighted by the recoupling coefficients).
U′ = Adapt.adapt(typeof(data_dst), StridedView(U))
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.

What is the point of wrapping U in a StridedView here before the adapt call? data_dst is not a StridedView at this point, right? Is this equivalent to StridedView(Adapt.adapt(typeof(data_dst), U))?

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.

It is in spirit, the reason for the change in order is that storagetype yields something with ndims = 1, while U is a Matrix, so we have the freedom to be slightly more liberal with the strided implementation but the standard CuArray doesn't capture that

# using a trivial permutation so the layout is canonical before the matmul.
@inbounds for (i, struct_src_i) in enumerate(structs_src)
TO.tensoradd!(
sreshape(buffer_src[:, i], sz_src), StridedView(data_src, sz_src, struct_src_i...),
Copy link
Copy Markdown
Member

@Jutho Jutho May 29, 2026

Choose a reason for hiding this comment

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

This contains a getindex that relies on StridedView producing a view, so if we ever change this behavior, this is where we will have to be careful.

# 1. Extract: copy each source block into column i of buffer_src as a flat vector,
# using a trivial permutation so the layout is canonical before the matmul.
@inbounds for (i, struct_src_i) in enumerate(structs_src)
TO.tensoradd!(
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.

Does it make sense to simply call copy! for the ptriv case?

p, false, α * coeff, β, backend, allocator
)
else # Multi-tree block: pack → recoupling matmul → unpack.
rows, cols = size(U)
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.

Is there ever a case where this is not square? If so, does it make sense to do the trivial permutation on the largest of the two (src vs dest), and the non-trivial permutation on the smallest?

p, false, α, β, backend, allocator
)
end
@lock buffer_lock TO.tensorfree!(buffer, allocator)
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.

I have forgotten how the allocators work. It is now fine that we free buffers in on order that is not the exact reverse as the order in which they were allocated?

# buffers have to be created without race condition: err on the side of caution with a lock
buffer_lock = Threads.ReentrantLock()

OhMyThreads.@tasks for src in fusionblocks(tsrc)
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.

There is some code duplication between this generic implementation, and the one for GenericTreeTransformer below (hence my questions there also apply here). But I think the code duplication is unavoidable (and anyway quite limited).

return maximum(transformer.data; init = 0) do (basistransform, structures_dst, _)
return prod(structures_dst[1]) * size(basistransform, 1)
end
end
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.

Is this function used anywhere? I couldn't find a single calling instance? The add_transform_kernel! computes the buffer size manually.

Copy link
Copy Markdown
Member

@Jutho Jutho left a comment

Choose a reason for hiding this comment

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

Ok, this looks great. I've left a few questions, but mostly just to get better understanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants