Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How finalizers |> work #165

Open
RainerHeintzmann opened this issue Mar 27, 2023 · 5 comments
Open

How finalizers |> work #165

RainerHeintzmann opened this issue Mar 27, 2023 · 5 comments
Labels

Comments

@RainerHeintzmann
Copy link

I am having some trouble to grasp how to limit the action of the einsum:

julia> a = ones(3,3); b= ones(3,3);

julia> @tullio y[j,i] := a[i,k] * b[j,k]
3×3 Matrix{Float64}:
 3.0  3.0  3.0
 3.0  3.0  3.0
 3.0  3.0  3.0

julia> @tullio y[j,i] += a[m,j] * b[m,i]
3×3 Matrix{Float64}:
 6.0  6.0  6.0
 6.0  6.0  6.0
 6.0  6.0  6.0

julia> @tullio y[j,i] := a[i,k] * b[j,k] + a[m,j] * b[m,i]
3×3 Matrix{Float64}:
 18.0  18.0  18.0
 18.0  18.0  18.0
 18.0  18.0  18.0

julia> @tullio y[j,i] := (a[i,k] * b[j,k] |> identity) + (a[m,j] * b[m,i] |> identity)
3×3 Matrix{Float64}:
 18.0  18.0  18.0
 18.0  18.0  18.0
 18.0  18.0  18.0

my current implementation uses @tullio by splitting the expression into multiple @tullio calls as in the first two examples. 6*ones(3,3) is the correct result, which I want to achieve. Yet it seems like a single expression (applied over a large array) should be faster. But the 3rd result yields not what I expected. Rethinking, I can understand that it is probably interpreted by moving both sums (over k and over l) to the very outside. To limit this effect, I thought the |> operation would limit ("finalize") the action, but this does not seem to be the case. What did I get wrong here? I guess in this example one could use the same index for m and k, but in my case this is not possible, since the ranges differ.

@mcabbott
Copy link
Owner

Rethinking, I can understand that it is probably interpreted by moving both sums (over k and over l) to the very outside.

Yes, this is exactly right. Unlike Einstein it does not know that + is special, it sees y[j,i] := f(g(a[i,k], b[j,k]), g(a[m,j], b[m,i])) as something so sum over k and j.

The macro always generates just one loop nest. All that you can do with |> is apply an operation later in the nest. The canonical example is @tullio y[i] := x[i,k]^2 |> sqrt which makes

for i in axes(y,1)  # outer loops for LHS
  tmp = 0
  for k in axes(x,2)  # inner loops are the sum
    tmp += x[i,k]^2
  end
  y[i] = sqrt(tmp)  # sqrt moved later
end

Maybe "finaliser" is the wrong word, but that's all it does. I see what you're hoping for but that requires a more complicated set of loops which Tullio doesn't understand. I think the macro has not noticed the |> at all (since it's not at top level) and hence calls Base's version which does nothing here. Probably it should throw an error instead.

@RainerHeintzmann
Copy link
Author

Thanks for the explanation. This makes sense. I found a way to write it, but I guess this is still doing pretty much the same as the first example above.

julia> @btime $c .= (@tullio $y[j,i] := $a[i,k] * $b[j,k]) .+ (@tullio $y[j,i] := $a[m,j] * $b[m,i]);
  673.596 ms (4 allocations: 15.26 MiB)

The timings and memory are for 1k x1k arrays. The memory consumption got me a little worried, which is why I also tried a simple matrix multiplication:

julia> @btime c .= $a * transpose($b);
  9.269 ms (3 allocations: 7.63 MiB)

julia> @btime @tullio c[j,i] = $a[i,k] * $b[j,k];
  549.171 ms (9 allocations: 176 bytes)

Memory is no problem here, but speed is (probably cache usage?).
Is this (Tullio being 5x slower) a known issue?

@RainerHeintzmann
Copy link
Author

The non-tullio way of writing this is also using 15 Mb, but is faster:

julia> @btime $c .= $a*transpose($b) .+ transpose($a)*$b;
  19.053 ms (4 allocations: 15.26 MiB)

I guess this may have to do with the magic of efficient (<O(N²)) implementation of matrix multiplications.

@mcabbott
Copy link
Owner

If you have c then this will avoid the allocations:

mul!(c, a, transpose(b))
mul!(c, transpose(a), b, true, true)

So will things like @tullio y[j,i] += a[m,j] * b[m,i], with = or += but not :=.

For straight matrix multiplication, Tullio will usually lose to more specialised routines. See e.g. this graph: https://github.com/JuliaLinearAlgebra/Octavian.jl Around size 100, it suffers from the overhead of using Base's threads. Around size 3000, it suffers from not knowing about some optimisations. (I don't think <N^3 algorithms like Strassen are actually used in BLAS, but not very sure.) Tullio's main purpose in life is handling weird contractions which aren't served at all by such libraries, or which would require expensive permutedims operations before/after. These are where it can be 5x faster sometimes.

@RainerHeintzmann
Copy link
Author

RainerHeintzmann commented Mar 31, 2023

Thanks. Of course <O(N^3) is what I meant. Interesting to know that Strassen or alike are not actually used in BLAS as discussed here:
I was using Tullio for exactly this use-case, a weired contraction. See this code.
Yet it seems to be hard to avoid allocations or storing intermediate results in large arrays.

@mcabbott mcabbott changed the title finalizers How finalizers |> work May 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants