Skip to content

Redo add_transform! for GPU backed Tensors to be much, much faster - #509

Open
kshyatt wants to merge 3 commits into
mainfrom
ksh/add_transform
Open

Redo add_transform! for GPU backed Tensors to be much, much faster #509
kshyatt wants to merge 3 commits into
mainfrom
ksh/add_transform

Conversation

@kshyatt

Copy link
Copy Markdown
Member

Should close#508

The short version of what I've done here is rework add_transform! into two kernels with associated helper structures, one for the Abelian case (which is quite simple) and one for the more generic case. Instead of parallelizing over the blocks, which are of pretty different sizes, I parallelize over the output data in data_dst. This means a lot more annoying bookkeeping but also a much more uniform workload across the GPU and better usage of the large number of available threads. There are still probably some performance optimizations to be done but I thought this was a) already hard enough to understand and b) pretty compelling!

Here are the results for the sample script I linked in the issue above, on an AMD MI210:

GPU permute time (ms)

casetreeselemsbytesmainksh/add_transformspeedup
SU2 iPEPO tensor423292.6 KiB1.9150.03358×
trivial symmetry1960475.0 KiB0.0220.0201.1×
SU2 jmax=1426725.3 KiB1.9640.03360×
SU2 jmax=2323516840.4 KiB13.8180.034406×
SU2 jmax=3136421824170.5 KiB53.5370.0461164×
U170222617.4 KiB1.0640.02053×

Cost per fusion tree (GPU, µs/tree)

casetreesmainksh/add_transform
SU2 iPEPO tensor4245.60.79
SU2 jmax=14246.80.79
SU2 jmax=232342.80.105
SU2 jmax=3136439.20.034
U17015.20.29

So this should hopefully finally let people doing complicated stuff (e.g. anything involving SU(2)) really benefit from the GPU. All the tests passed for me locally.

I still have some lingering questions about where stuff should live. I added some new caches of GPU objects as well to avoid sending things back to the GPU that don't need to go, but maybe those should live in the main package?

@kshyatt
kshyatt requested a review from lkdvosAugust 18, 2026 12:29
@kshyatt

Copy link
Copy Markdown
MemberAuthor

I'm also running the GPU branch of PEPSKit.jl against this to see how much/if it helps

@kshyatt

Copy link
Copy Markdown
MemberAuthor

Well, the short story for now is it doesn't help much because nearly all the time in our PEPSKit runs is spent on the CPU (GPU utilization is quite low). I'll look into that separately but I still think this is a nice bit of progress :)

@codecov

codecovBot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing linesCoverage Δ
ext/TensorKitGPUArraysExt.jl95.41% <100.00%> (+6.32%)⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kshyatt

Copy link
Copy Markdown
MemberAuthor

OK I've got one more thing to push here to make it play nicely with PEPSKit (cache-related) but most of this is ready to go I think

@kshyatt

Copy link
Copy Markdown
MemberAuthor

OK!!! I modified stuff a bit for the DEVICE_TRANSFORMER_CACHE because we need the GPUArrays caching allocator to really see the benefits of this on PEPSKit (otherwise we choke to death on allocations). The @uncached here is to protect the cached GPU-side transformer infos from getting reaped while we still expect them to be live.

@lkdvoslkdvos left a comment

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.

have only looked at the abelian part for now, looks really cool! I didn't really realize that indeed we can just spawn a thread per element. I am definitely wondering how this affects the case where the tensors are a bit larger and the number of blocks not so severe, for example what would effectively be the case of more MPS-like contractions, for which it might be reasonable to try and generalize/run the benchmarks that are in this repository on GPU as well to get a sense about overall performance implications?

Given that every element is read/written exactly once, it could also be cool to just express these numbers in terms of actual read/write bandwidth percentages, for which 🤖 might be able to help? Not that I'm expecting or requiring any huge results, but I'd be very interested to know how much is left on the table


# largest `i` with `offsets[i] <= w`. This corresponds to the
# block which this kernel thread will work on.
@inline function _searchblock(offsets, w)

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 Base.searchsortedlast or Base.searchsortedfirst?

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.

edit: I see now that this is used inside the kernel, which I assume is why that has to be like this

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Should I add a comment about this to make it clearer?

Comment threadext/TensorKitGPUArraysExt.jl Outdated

# Cartesian coordinates of the `w`-th (0-based) entry of a dense subblock of shape `sz`.
# Computed once per thread and then reused for every strided view of that subblock.
# This avoids `StridedView` redoing these divisions on every single element access.

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.

This is slightly confusing to me, isn't this still computed in each kernel call below (line 371)? Keep in mind I might be completely misreading kernels here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's computed just once per kernel thread, though, which becomes more important for the generic case

@kshyatt

Copy link
Copy Markdown
MemberAuthor

I am definitely wondering how this affects the case where the tensors are a bit larger and the number of blocks not so severe, for example what would effectively be the case of more MPS-like contractions, for which it might be reasonable to try and generalize/run the benchmarks that are in this repository on GPU as well to get a sense about overall performance implications?

Yeah, it's a really good question. I focused on the cases I looked at in the linked issue, but if we have some others I can run I will certainly do that to get a clearer picture.

@kshyatt

Copy link
Copy Markdown
MemberAuthor

I didn't really realize that indeed we can just spawn a thread per element.

I did do a bit of testing with these kernels on CPU arrays and there's no real benefit there, this approach is really GPU specific I guess.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GPU-backed TensorMappermute!braid is extremely inefficient

2 participants

@kshyatt@lkdvos