Giter VIP home page Giter VIP logo

einsum.jl's Introduction

Einsum.jl

Package Build Package Status
Build Status License
Einsum Project Status: Inactive - The project has reached a stable, usable state but is no longer being actively developed; support/maintenance will be provided as time allows. - help wanted!

This package exports a single macro @einsum, which implements similar notation to the Einstein summation convention to flexibly specify operations on Julia Arrays, similar to numpy's einsum function (but more flexible!).

For example, basic matrix multiplication can be implemented as:

@einsum A[i, j] := B[i, k] * C[k, j]

To install: Pkg.add("Einsum"), or else pkg> add Einsum after pressing ] on Julia 0.7 and later.

Documentation

Basics

If the destination array is preallocated, then use =:

A = ones(5, 6, 7) # preallocated space
X = randn(5, 2)
Y = randn(6, 2)
Z = randn(7, 2)

# Store the result in A, overwriting as necessary:
@einsum A[i, j, k] = X[i, r] * Y[j, r] * Z[k, r]

If destination is not preallocated, then use := to automatically create a new array for the result:

X = randn(5, 2)
Y = randn(6, 2)
Z = randn(7, 2)

# Create new array B with appropriate dimensions:
@einsum B[i, j, k] := X[i, r] * Y[j, r] * Z[k, r]

What happens under the hood

To execute an expression, @einsum uses Julia's metaprogramming capabilities to generate and execute a series of nested for loops. To see exactly what is generated, use @macroexpand (or @expand from MacroTools.jl):

@macroexpand @einsum A[i, j] := B[i, k] * C[k, j]

The output will look much like the following (note that we "sum out" over the index k, since it only occurs multiple times on the right hand side of the equation):

# determine type
T = promote_type(eltype(B), eltype(C))

# allocate new array
A = Array{T}(undef, size(B))

# check dimensions
@assert size(B, 2) == size(C, 2)

# main loop
@inbounds begin # skip bounds-checking for speed
    for i = 1:size(B, 1), j = 1:size(C, 2)
        s = zero(T)
        for k = 1:size(B,2)
            s += B[i, k] * C[k, j]
        end
        A[i, j] = s
    end
end

The actual generated code is a bit more verbose (and not neatly commented/formatted), and will take care to use the right types and keep hygienic.

You can also use updating assignment operators for preallocated arrays. E.g., @einsum A[i, j, k] *= X[i, r] * Y[j, r] * Z[k, r] will produce something like

for k = 1:size(A, 3)
    for j = 1:size(A, 2)
        for i = 1:size(A, 1)
            s = 0.0
            for r = 1:size(X, 2)
                s += X[i, r] * Y[j, r] * Z[k, r]
            end
            # Difference: here, the updating form is used:
            A[i, j, k] *= s
        end
    end
end

Rules for indexing variables

  • Indices that show up on the right-hand-side but not the left-hand-side are summed over
  • Arrays which share an index must be of the same size, in those dimensions

@einsum iterates over the extent of the right-hand-side indices. For example, the following code allocates an array A that is the same size as B and copies its data into A:

@einsum A[i,  j] := B[i, j]  # same as A = copy(B)

If an index appears on the right-hand-side, but does not appear on the left-hand-side, then this variable is summed over. For example, the following code allocates A to be size(B, 1) and sums over the rows of B:

@einsum A[i] := B[i, j]  # same as A = dropdims(sum(B, dims=2), dims=2)

If an index variable appears multiple times on the right-hand-side, then it is asserted that the sizes of these dimensions match. For example,

@einsum A[i] := B[i, j] * C[j]

will check that the second dimension of B matches the first dimension of C in length. In particular it is equivalent to the following code:

A = zeros(size(B, 1))
@assert size(B, 2) == size(C, 1)

for i = 1:size(B, 1), j = 1:size(B, 2)
    A[i] += B[i, j] * C[j]
end

So an error will be thrown if the specified dimensions of B and C don't match.

Offset indexing

@einsum also allows offsets on the right-hand-side, the range over which i is summed is then restricted:

@einsum A[i] = B[i - 5]

@vielsum

This variant of @einsum will run multi-threaded on the outermost loop. For this to be fast, the code must not introduce temporaries like s = 0 in the example above. Thus for example @expand @vielsum A[i,j,k] = X[i,r]*Y[j,r]*Z[k,r] results in something equivalent to @expand-ing the following:

Threads.@threads for k = 1:size(A,3)
    for j = 1:size(A,2)
        for i = 1:size(A,1)
            A[i,j,k] = 0.0
            for r = 1:size(X,2)
                A[i,j,k] += X[i,r] * Y[j,r] * Z[k,r]
            end
        end
    end
end

For this to be useful, you will need to set an environment variable before starting Julia, such as export JULIA_NUM_THREADS=4. See the manual for details, and note that this is somewhat experimental. This will not always be faster, especially for small arrays, as there is some overhead to dividing up the work.

At present you cannot use updating assignment operators like += with this macro (only = or :=) and you cannot assign to a scalar left-hand-side (only an array). These limitations prevent different threads from writing to the same memory at the same time.

@einsimd

This is a variant of @einsum which will put @simd in front of the innermost loop, encouraging Julia's compiler vectorize this loop (although it may do so already). For example @einsimd A[i,j,k] = X[i,r]*Y[j,r]*Z[k,r] will result in approximately

@inbounds for k = 1:size(A,3)
    for j = 1:size(A,2)
        for i = 1:size(A,1)
            s = 0.0
            @simd for r = 1:size(X,2)
                s += X[i,r] * Y[j,r] * Z[k,r]
            end
            A[i,j,k] = s
        end
    end
end

Whether this is a good idea or not you have to decide and benchmark for yourself in every specific case. @simd makes sense for certain kinds of operations; make yourself familiar with its documentation and the inner workings of it in general.

Other functionality

The @einsum macro can implement function calls within the nested for loop structure. For example, consider transposing a block matrix:

z = Any[rand(2,2) for i=1:2, j=1:2]
@einsum t[i,j] := transpose(z[j,i])

This produces a for loop structure with a transpose function call in the middle. Approximately:

for j = 1:size(z,1)
    for i = 1:size(z,2)
        t[i,j] = transpose(z[j,i])
    end
end

This will work as long the function calls are outside the array names. Again, you can use @macroexpand to see the exact code that is generated.

The output need not be an array. But note that on Julia 0.7 and 1.0, the rules for evaluating in global scope (for example at the REPL prompt) are a little different -- see this package for instance (which is loaded in IJulia notebooks). To get the same behavior as you would have inside a function, you use a let block:

p = rand(5); p .= p ./ sum(p)
let
    global S
    @einsum S := - p[i] * log(p[i])
end

Or perhaps clearer, explicitly define a function:

m(pq, p, q) = @einsum I := pq[i,j] * log(pq[i,j] / p[i] / q[j])

m(rand(5,10), rand(5), rand(10))

Related Packages:

  • TensorOperations.jl has less flexible syntax (only allowing strict Einstein convention contractions), but can produce much more efficient code. Instead of generating “naive” loops, it transforms the expressions into optimized contraction functions and takes care to use a good (cache-friendly) order for the looping.

  • ArrayMeta.jl aims to produce cache-friendly operations for more general loops (but is Julia 0.6 only).

einsum.jl's People

Contributors

ahwillia avatar carstenbauer avatar dfdx avatar mcabbott avatar ntezak avatar phipsgabler avatar tkelman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

einsum.jl's Issues

Einsum over variable indices

Since @einsum interface is a macro, I wonder if it is possible to take variable as input indices? like

julia> Is = (1,2,3)
(1, 2, 3)

julia> Js = (2,3,4)
(2, 3, 4)

julia> Ks = (1,4)
(1, 4)

julia> einsum(((Is, Js), Ks), A, B, C)

Does not work with Unitful.jl?

For example, I have the following sum rule:

@einsum T′[i, j] := T[k, l] * Q[i, k] * Q[j, l]

and the macro-expanded code for above is:

julia> @macroexpand @einsum T′[i, j] := T[k, l] * Q[i, k] * Q[j, l]
quote
    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:207 =#
    local var"##T#342" = promote_type(eltype(T), eltype(Q), eltype(Q))
    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:208 =#
    T′ = Array{var"##T#342"}(undef, size(Q, 1), size(Q, 1))
    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:209 =#
    if size(T, 2) == size(Q, 2)
        nothing
    else
        Base.throw(Base.AssertionError("size(T, 2) == size(Q, 2)"))
    end
    if size(T, 1) == size(Q, 2)
        nothing
    else
        Base.throw(Base.AssertionError("size(T, 1) == size(Q, 2)"))
    end
    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:212 =#
    let i, j, k, l
        #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:213 =#
        begin
            $(Expr(:inbounds, true))
            local var"#170#val" = for j = 1:size(Q, 1)
                        #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:278 =#
                        for i = 1:size(Q, 1)
                            #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:278 =#
                            begin
                                #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:176 =#
                                local var"##s#343" = zero(var"##T#342")
                                #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:177 =#
                                for l = 1:size(T, 2)
                                    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:278 =#
                                    for k = 1:size(T, 1)
                                        #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:278 =#
                                        var"##s#343" += T[k, l] * Q[i, k] * Q[j, l]
                                        #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:279 =#
                                    end
                                    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:279 =#
                                end
                                #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:178 =#
                                T′[i, j] = var"##s#343"
                            end
                            #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:279 =#
                        end
                        #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:279 =#
                    end
            $(Expr(:inbounds, :pop))
            var"#170#val"
        end
    end
    #= ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:216 =#
    T′
end

However, in this line:

local var"##T#342" = promote_type(eltype(T), eltype(Q), eltype(Q))

It does not consider that T, Q may have units defined in Unitful.jl. For example, say T is a matrix filled with quantities like 1u"m", and Q is filled with quantities like 2.0u"kg". Then it will return type Quantity{Float64}:

julia> S = promote_type(typeof(1u"m"), typeof(2.0u"kg"), typeof(2.0u"kg"))
Quantity{Float64}

The final eltype of the results will have dimension m * kg * kg,

As I mentioned in PainterQubits/Unitful.jl#616, S will be used in

local var"##s#343" = zero(var"##T#342")

as the initial summation value. However, zero for Quantity{Float64} is not defined. So it will throw an ArgumentError.

Referencing external variables vs dummy variables within @einsum

I am in need of a package very much like Einsum.jl. Please refer to my julia-users posting, which is here:

https://groups.google.com/forum/#!topic/julia-users/CYGxXh1jNDg

as well as documentation of my vaporware, which is here:

https://github.com/StephenVavasis/indicial.jl

However, Einsum is missing a key feature that I need, namely, indirect addressing. In other words, I need to be able to transform a loop like

     x[:] = y[:] + z[i[:]]

into a for-loop.

Is there any possibility of adding indirect addressing?

Two other issues:

Broadcasting seems to work

   A[a,b] = x[a]  #OK

except in the case of no indices on the RHS, e.g.,

    x[a] = 0.0 #not OK

Also, the package does not seem to run under 0.5?

Thanks,
Steve Vavasis

Handle indices that appear on lhs but not rhs better

This

@einsum A[i,j] := B[i]

Throws an error that could be better explained (though it isn't terrible):

ERROR: UndefVarError: A not defined
 in macro expansion; at /Users/alex/.julia/v0.5/Einsum/src/Einsum.jl:127 [inlined]
 in anonymous at ./<missing>:?

Taking the trace

Heya,
I have some issues taking the trace of a matrix or other complete reductions.
I assumed this should work:

julia> using Einsum;

julia> a = [1 0; 0 1]
2×2 Array{Int64,2}:
 1  0
 0  1

julia> @einsum b := a[i,i]
0

But that gives me the wrong result while

julia> b = zeros();

julia> @einsum b[] = a[i,i]
0-dimensional Array{Float64,0}:
2.0

works but not

julia> b = zeros();

julia> @einsum b = a[i,i]
0-dimensional Array{Float64,0}:
0.0

A possible fix could be replacing zero with zeros (to get a 0-dim array) here:

:($(lhs_arrays[1]) = zero($T))

But I'm not sure if that breaks something else.
At least @einsum b[] := a[i,i] would work then.

Let me know if I'm simply using the package wrong.
Cheers

edit: The issue is not present if the macro is in a function, i.e.

foo(a) = @einsum b := a[i,i]

I guess then that it has to do with macro-magic which I don't grok at all I'm afraid.

Performance

Compared to numpy.einsum, Einsum.jl seems to be quite slow. I wonder if there is some room for improvements on this side:

numpy

import numpy as np
import timeit

# P = np.random.random((20, 15, 100, 30, 2))

A = np.random.random((20, 15, 100, 5))
H = np.random.random((30, 5))
C = np.random.random((2, 5))


def run():
    return np.einsum('abfj,tj,cj->abftc', A, H, C)

times = timeit.Timer(run).timeit(number=10)

print times

Elapsed time: 1.44563603401s

Julia

using Einsum

P = zeros(20,15,100,30,2); 

A = randn(20,15,100,5);
H = randn(30,5);
C = randn(2,5);

tic()
for i = 1:10
  @einsum P[a,b,f,t,c] = A[a,b,f,j]*H[t,j]*C[c,j]
end
toc()

elapsed time: 85.405141333 seconds

Local variable leaking?

I'm not a macro person, so not sure exactly how this happens (hygiene?), but I observe the following:

using Einsum
function test(x::Vector{T}, y::Vector{T}) where T
   @einsum z := x[i]*y[i]
   return z
end
test(rand(3), rand(3))

gives the warning: WARNING: local variable T conflicts with a static parameter in test at . . .

Maybe this is unavoidable? Maybe not? But then T could be renamed so it can be used as a type parameter?

Clarification about einsimd

When thinking about how to document @einsimd, I noticed that @simd only gets added to inner loops of contractions, but not to "straight" loops, e.g.

julia> @macroexpand Einsum.@einsimd a[i] = b[i]
quote  
    let
        begin 
            $(Expr(:inbounds, true))
            begin  
                local i 
                for i = 1:min(size(a, 1), size(b, 1)) 
                    a[i] = b[i]
                end
            end
            $(Expr(:inbounds, :pop))
        end
    end
    a
end

Shouldn't this be vectorized, too?

More general: would the right behaviour for @einsimd be to always add @simd to the innermost loop?

And for documentation (since I don't know much about SIMD): for which loops or kinds of operations does this actually make sense? Any good ressources I can link to?

Support struct fields for tensors

If, say, params is a struct, it should be possible to write:

@einsum params.x[i,j,k] = params.y[i,j]*params.z[j, k]

This does seem to work in TensorOperations.jl

Output type inference

Since Einsum allows functions, it would be neat if it were smart enough to infer their output types. But it's not:

julia> B = ones(2);

julia> @einsum A[i] := (x->x+im)(B[i])
ERROR: InexactError: Float64(1.0 + 1.0im)

This is because it just promotes the array eltypes. Perhaps it should compute the first element, if there are functions other than *, or something?

Define type of aggregate since not all types are closed under addition or multiplication

MWE:

using DynamicPolynomials
julia> using Einsum

julia> @ncpolyvar a[1:2,1:2]
(PolyVar{false}[a₁₋₁ a₁₋₂; a₂₋₁ a₂₋₂],)

julia> @ncpolyvar b[1:2,1:2]
(PolyVar{false}[b₁₋₁ b₁₋₂; b₂₋₁ b₂₋₂],)

julia> @einsum c[x,y] := a[x,z]*b[z,y]
ERROR: InexactError: convert(PolyVar{false}, a[1,1]*b[1,1] + a[1,2]*b[2,1])
Stacktrace:
 [1] convert(#unused#::Type{PolyVar{false}}, p::Polynomial{false, Int64})
   @ MultivariatePolynomials ~/.julia/packages/MultivariatePolynomials/KMk2o/src/conversion.jl:58
 [2] setindex!(::Matrix{PolyVar{false}}, ::Polynomial{false, Int64}, ::Int64, ::Int64)
   @ Base ./array.jl:841
 [3] top-level scope
   @ ~/.julia/packages/Einsum/AVMOj/src/Einsum.jl:178

If it was possible to define the aggregation type to Polynomial{false,Int64} this would be solveable.

Return value of assigned result

This is a different behaviour from TensorOperations.jl:

julia> @tensor r[k] := x[i] * M[k, i, j] * x[j]
5-element Array{Float64,1}:
 2.11926
 2.22626
 2.99243
 2.30047
 1.65922

julia> @einsum r[k] := x[i] * M[k, i, j] * x[j]

julia> r
5-element Array{Float64,1}:
 2.11926
 2.22626
 2.99243
 2.30047
 1.65922

I find it more natural if the result of an assignment is also immediately returned, that's basically what happens normally in Julia. Or was that a deliberate choice?

Support for qualified function names

On Julia 0.5:

x = rand(3)
inc(x) = x .+ 1

@einsum y[i] := inc(x[i])  # ==> ok
@einsum y[i] := Main.inc(x[i])  # ==> ERROR: syntax: malformed expression

It turns out the function call is transformed into Main.(inc)(x[i]) which is exactly the source of the error. Oddly enough, pure _einsum() doesn't have this issue and generates absolutely correct code.

It's also worth to mention that on Julia 0.4 the error is different:

ERROR: TypeError: getfield: expected Symbol, got Function
in anonymous at /home/slipslop/.julia/v0.5/Einsum/src/Einsum.jl:217

Do better at allocating array types

Should be an easy fix. Opening this issue to remind myself:

B,C = ones(Int,5),randn(5)
@einsum A[i,j] := B[i]*C[j]

Causes:

ERROR: InexactError()
 in setindex!(::Array{Int64,2}, ::Float64, ::Int64, ::Int64) at ./array.jl:378
 in macro expansion; at /Users/alex/.julia/v0.5/Einsum/src/Einsum.jl:158 [inlined]
 in anonymous at ./<missing>:?

Because A is allocated stupidly:

A = Array(eltype(B),size(B,1),size(C,1))

Should use promote_type(eltype(B),eltype(C))

Checklist for v0.2.0

I have had a mental checklist of things to implement. All of this should be doable in the near-term / mid-term for the next release:

  • Add docs for @einsimd
  • Support for inplace operations
    • Initial PR #7
    • Docs added
  • Support for offsets in indices, #6, #12
    • with constants, @einsum y[i] = y[i+3]
    • with variables, @einsum y[i] = y[i+offset]
  • More flexible operations in the inner loop. (Below are some minimal examples)
    • @einsum y[i] = 2*y[i]
    • @einsum x[i] = x[i] + 5

Don't sum over an index for terms that lack it

Suppose I have the following case

julia> using Einsum;
julia> a = [1, 2];
julia> b = [1 2; 3 4];
julia> c = a+b*a  # what I would like to compute
2-element Array{Int64,1}:
  6
 13
julia> @einsum c[i] := a[i] + b[i,j] * a[j]  # this doesn't work
2-element Array{Int64,1}:
  7
 15
julia> @einsum c[i] := b[i,j] * a[j];  # breaking up the problem works
julia> @einsum c[i] += a[i]
2-element Array{Int64,1}:
  6
 13

It seems terms that don't include an index are still summed over that index. From my understanding of Einstein notation, this isn't supposed to happen. Is there a way to avoid the hack above? For reference, TensorOperations.jl handles the sum as expected.

julia> using TensorOperations;
julia> @tensor c[i] := a[i] + b[i,j] * a[j]
2-element Array{Int64,1}:
  6
 13

Support for in-place operations?

Hi, would it fit to the overall vision for the package to support in-place operations for pre-allocated arrays?

E.g., 
A = randn(5,6,7);

X = randn(5,2);
Y = randn(6,2);
Z = randn(7,2);

@einsum A[i,j,k] += X[i,r]*Y[j,r]*Z[k,r]

The code changes are minimal (I have already added them as well as testing code in my fork).
Here is the commit:
ntezak/Einsum.jl@73d087c

Julia 0.6 deprecation warning messages

When using Einsum in Julia 0.6 we get the message:

WARNING: Array{T}(::Type{T}, m::Int, n::Int, o::Int) is deprecated, use Array{T}(m, n, o) instead.

Support for offsets in indices

Hi, for my use-cases (quantum dynamics solvers) it would be super useful to be able to specify integer offsets in the indices. As an example

E.g., 
A = zeros(10);

X = randn(10);
Y = randn(10);

@einsum A[j] = X[j]*Y[j+3]
@assert isapprox(A, [X.*Y[4:end];zeros(3)])

Here the macro ought to infer that effectively A[1:7] = X[1:7] .* Y[4:10] and A[8:10] = 0.
Taken just by itself it may not be obvious why this is more useful than just using subarrays but especially in combination with in-place operations (see issue #5) this could be quite powerful.

If this seems of general use, I would be happy to take care of the implementation.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.