Skip to content

GPU-backed TensorMap permute! braid is extremely inefficient #508

Description

@kshyatt

tl;dr We are doing a separate kernel launch for every element of the fusion tree, which is SUPER wasteful especially for otherwise small tensors.

Here's a sample script:

using TensorKit, AMDGPU, Adapt, LinearAlgebra, Printf

function warmup(seconds = 3.0)
    X = AMDGPU.rand(Float32, 2048, 2048)
    t0 = time()
    while time() - t0 < seconds
        X * X
    end
    AMDGPU.synchronize()
    return nothing
end

function bench(f, n; reps = 7)
    f(); AMDGPU.synchronize()
    best = Inf
    for _ in 1:reps
        AMDGPU.synchronize()
        e = @elapsed begin
            for _ in 1:n; f(); end
            AMDGPU.synchronize()
        end
        best = min(best, e / n)
    end
    return best * 1e3
end

nfusiontrees(t) = length(collect(TensorKit.fusiontrees(t)))
nstored(t)      = sum(prod(size(b)) for (_, b) in blocks(t))

function report(name, t_cpu, perm)
    t_gpu = adapt(ROCArray, t_cpu)
    nb, nf, ne = length(collect(blocks(t_cpu))), nfusiontrees(t_cpu), nstored(t_cpu)

    c = bench(() -> permute(t_cpu, perm; copy = true), 200)
    g = bench(() -> permute(t_gpu, perm; copy = true), 200)

    # device allocations issued by ONE permute
    AMDGPU.synchronize()
    s0 = copy(AMDGPU.alloc_stats)
    permute(t_gpu, perm; copy = true)
    AMDGPU.synchronize()
    d = AMDGPU.alloc_stats - s0

    @printf("%-18s trees=%-5d elems=%-6d %-10s  CPU %7.3f ms  GPU %8.3f ms  GPU/CPU %6.2fx  allocs/permute=%d\n",
            name, nf, ne, Base.format_bytes(ne * 8), c, g, g / c, d.alloc_count)
    return nothing
end

println("=== permute cost vs. fusion-tree count (AMDGPU / MI210) ===")
println("(best-of-7 batches of 200; GPU warmed up first)\n")
warmup()

Vp = Vect[SU2Irrep](1//2 => 1)
Vv = Vect[SU2Irrep](0 => 1, 1 => 2)
A  = randn(Float64, Vp  Vp', Vv  Vv  Vv'  Vv')
perm6 = ((2, 3, 5, 6), (1, 4))
report("SU2 iPEPO tensor", A, perm6)

Wp, Wv =^2, ℂ^7
B = randn(ComplexF64, Wp  Wp', Wv  Wv  Wv'  Wv')
report("trivial symmetry", B, perm6)

println()
for nmax in 1:3
    V = Vect[SU2Irrep](j => 2 for j in 0:nmax)
    T = randn(Float64, Vp  Vp', V  V  V'  V')
    report("SU2 jmax=$nmax", T, perm6)
end

println()
Up = Vect[U1Irrep](1//2 => 1, -1//2 => 1)
Uv = Vect[U1Irrep](-1 => 2, 0 => 3, 1 => 2)
C = randn(Float64, Up  Up', Uv  Uv  Uv'  Uv')
report("U1", C, perm6)

Here are the results I see on an MI210:

SU2 iPEPO tensor   trees=42    elems=329    2.570 KiB   CPU   0.123 ms  GPU    2.144 ms  GPU/CPU  17.43x  allocs/permute=23
trivial symmetry   trees=1     elems=9604   75.031 KiB  CPU   0.045 ms  GPU    0.022 ms  GPU/CPU   0.50x  allocs/permute=1

SU2 jmax=1         trees=42    elems=672    5.250 KiB   CPU   0.095 ms  GPU    2.107 ms  GPU/CPU  22.11x  allocs/permute=23
SU2 jmax=2         trees=323   elems=5168   40.375 KiB  CPU   0.726 ms  GPU   15.136 ms  GPU/CPU  20.84x  allocs/permute=121
SU2 jmax=3         trees=1364  elems=21824  170.500 KiB  CPU   2.910 ms  GPU   57.840 ms  GPU/CPU  19.88x  allocs/permute=391

U1                 trees=70    elems=2226   17.391 KiB  CPU   0.053 ms  GPU    1.168 ms  GPU/CPU  21.95x  allocs/permute=1

The GPU is way way slower due to all the kernel launches. The allocs/permute number tells you how many allocations occurred per permute.

I think a good first step would be to try to fuse these into one launch if we can,

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions