A basic introduction to NumPy's einsum
ajcr.net
ajcr.net
https://einops.rocks/pytorch-examples.html shows how it can be used to implement various neural network architectures in a more simplified manor.
- better syntax (because Julia has proper macro/metaprogramming)
- faster
- automatically works with GPU arrays.It doesn't do any automatic optimization of the loops like some of the projects linked in this thread, but, it provides all the tools needed for humans to express the code in a way that a good compiler can turn it into really good code.
So usually, I rewrite my formulas into messy combinations of broadcasts, transposes and array multiplications. Is there a package or an algorithm that does this conversion automatically? It seems to be a pretty straightforward problem, at least for most expressions I use.
For example, the optimization of np.einsum has been passed upstream and most of the same features that can be found in this repository can be enabled with numpy.einsum(..., optimize=True).
So numpy should provide the same perf for most cases.https://stackoverflow.com/questions/18365073/why-is-numpys-e...
Did I misunderstood your comment?
The optimizer clearly tries to improve the performance, but in many cases, it doesn't seem to change anything. Let's simply multiply some matrices:
x, y = np.random.rand(200, 200, 200), np.random.rand(200, 200, 200)
I can do %timeit x@y
40.3 ms ± 2.52 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
or a naive %timeit np.einsum('bik,bkj->bij',x,y)
1.53 s ± 21.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
But even with optimization, I see %timeit np.einsum('bik,bkj->bij',x,y, optimize=True)
1.54 s ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
I'm not sure if I'm doing something wrong. %timeit (x_jax @ y_jax).block_until_ready()
579 µs ± 4.54 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit jnp.einsum('bik,bkj->bij',x_jax,y_jax, optimize=True).block_until_ready()
658 µs ± 1.38 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit jnp.einsum('bik,bkj->bij',x_jax,y_jax).block_until_ready()
660 µs ± 2.82 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)`optimize=True` is generally best when there are more than two tensors in the expression.
The issue of "slow loops" is entirely self-inflicted by some languages. It's getting ridiculous to still worry about this shit in 2022.
Simple loops can and should be just as fast as vectorized programs. When they are slower, it is 100% due to deliberate decisions by the language dessigners
https://stackoverflow.com/questions/1303182/how-does-blas-ge...
Why turn your back on all the carefully crafted optimizations?
I don't want to turn my back on the beautiful loop notation. For some algorithms, the loop notation is clearer than any "vectorized" version. It is absurd that the language penalizes you for that. Loops are alright.
@tullio C[i,j] := A[i,k] * B[k,j]On the CPU, the situation is much better. With some help from LoopVectorization.jl (which optimises micro-kernels) it will often beat OpenBLAS at matrix multiplication. The best-case scenario is an operation which would otherwise be permutedims plus matrix multiplication, for which it will often be several times faster, by fusing these.
The notation above is shared by some other packages. TensorOperations.jl is always decomposes to known kernels (including on the GPU) and OMEinsum.jl usually does so (with a fallback to loops), both more like einsum. TensorCast.jl is more like einops, just notation for writing reshape/permute/slice operations.
Here's a good video that explains why its so good: https://www.youtube.com/watch?v=pkVwUVEHmfI
Also check out Lucid Rains Github, who uses it extensively to build transformer architectures from scratch: https://github.com/lucidrains \
* Example: https://github.com/lucidrains/alphafold2/blob/d59cb1ea536bc5...
Readable if you're already familiar with einsum notation. Otherwise there's learning curve. An alternative to einsum is using multiple dot product and reshape ops, hopefully with each one having a comment - this would be a lot more readable imo.
It does occur to me, though, that you are probably talking about the elementwise product, not the dot product. The dot product includes a reduce_sum step and outputs a scalar.
We wrote an article with it once, 40th order in the Lagrangian, perhaps 50k pages of calculations when all printed. Amazing tool! Thanks Kasper!
I taught myself C before arriving (and, eg., esp. via stanford's programming paradigms course) -- if I hadnt, I would be resigned with my classmates to C code projected on OHP slides.
If I had my way today, the whole of any applied science would be taught with both mathematical and programming (python for convenience) notation.