How to write efficient matrix multiplication
gist.github.com
gist.github.com
> see what else can be done I recommend reading this paper by Kazushige Goto
After doing the easiest change of loop reordering for already significant perf improvements, the paper linked ("Anatomy on a high performance matrix multiplication") is really excellent.
It will walk you through all the optimizations like tiling for reducing L1, L2, L3, and TLB cache misses, and leverage vectorization.
Then for squeezing out even more perf I remember there's the things like loop prefetching, loop unrolling, which you would expect the compiler with the best optimizations flags even targeted for the native architecture to take care of automatically. But you realize that it's not necessarily true.
Also, only tangentially related, but if you can change your problem formulation to make the matrix of interest sparse(r), that can be a huge win. Two things that have helped me in the past:
- Rows or columns that have mostly the same (nonzero) coefficient throughout. You can normally do some simple substitution to turn them into zeros.
- Rows or columns that are naturally very similar to each other. You can often replace `y` with `y-x` or replace `x[i]` with `x[i]-x[i+1]` to turn one (or `n-1`) columns "mostly empty."
That paper is light on details but, for example, in R you can define the function for toeplitz multiplication as below. This has given the matrix multiplication part of my code a >100x speedup before.
'%t*%' <- function(A,v){
n <- nrow(A)
x <- as.matrix(c(A[1,], 0, A[1,][n:2]))
p <- c(v, rep(0, n))
h <- as.vector(fft(p)*fft(x))
out <- Re(pracma::ifft(h)[1:n])
return(matrix(out, n))
}
all.equal(A %t*% v, A %*% v) #TRUEOn the other hand, optimal speed and JavaScript are often different ball parks.
I threw together this thing for when I need some matrix stuff in a little script. Not fast, but flexible.
var vop = op=>( (a,b)=>( a.map((v,i)=>op(v,b[i])) ) );
var vdiff = vop((a,b)=>a-b);
var vadd = vop((a,b)=>a+b);
var vdot= (a,b)=>a.reduce( (ac,av,i)=>ac+=av*b[i],0);
var vlength = a=>Math.sqrt(vdot(a,a));
var vscale = (a,b)=>a.map(v=>v*b);
var vdistance = (a,b)=>vlength(vdiff(a,b));
var vnormalised = (a,b=1)=>vscale(a,b/vlength(a));
var project = (point, matrix) => matrix.map( p=>vdot(p,[...point,1]));
var transpose = matrix=> ( matrix.reduce(($, row) => row.map((_, i) => [...($[i] || []), row[i]]), []) );
var multiply = (a,b,...rest)=>(!b)?a:multiply(a.map( (p=>transpose(b).map(q=>vdot(p,q)))),...rest);
I look forward to the day when the sufficiently-smart JIT implements the optimal Matrix Multiplication from that. :-)[1]: https://www.mathworks.com/help/matlab/ref/mldivide.html
Sparse matrices do not have that luxury. Sparse matrix ops have access patterns that entirely depend on your data. You simply cannot make a library that covers every possible distribution of non-zeroes in an optimal way. At best you have a toolbox of a large number of representations and algorithms that you choose for each different use-case. Engineering simulations and solvers can have block or cluster patterns of non-zeroes, which can be exploited by doing dense operations for the dense blocks. Large real-world graphs often have an exponential non-zero distribution and are often extremely sparse, which might need an hash-based join algorithm for an axpy-kernel. Are you running a tight CG-solver loop? Then you might want to bundle multiple spmv-kernels to operate multiple vectors on the same matrix (for similar reasons as states in the original article). You are distributing with Spark? Unless your non-zero distribution is uniform, your matrix is really hard to partition evenly. If your matrix never changes, you may consider preprocessing using a partitioner (METIS, Mondriaan).
In short, it is highly unlikely that the library you are using is anywhere near optimal for your use case. Do explore alternatives based on your domain-insights, it is not uncommon to net double digit performance improvements.
I was very surprised when I first learned that's not strictly true, because of denormal numbers[1]. Here's a session in ipython --pylab (with MKL), demonstrating a 200x slow-down for matrix multiplication with tiny numbers. Crazy!
In [1]: A = randn(1000, 1000)
In [2]: %timeit A @ A
25.9 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [3]: A *= 1e-160
In [4]: %timeit A @ A
5.52 s ± 21.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
You hit denormal numbers more quickly with single-precision floats. I have been bitten by this issue in the wild a couple of times now, and seen a couple of other people with it too. Sometimes denormals are created internally in algorithms, when you didn't think your input matrices were that small.[1] https://en.wikipedia.org/wiki/Denormal_number#Performance_is...
#define CSR_FLUSH_TO_ZERO (1 << 15)
unsigned csr = __builtin_ia32_stmxcsr();
csr |= CSR_FLUSH_TO_ZERO;
__builtin_ia32_ldmxcsr(csr);
from https://stackoverflow.com/a/8217313/ seems to work for me with gcc. I don't know how widely supported it would be.The only instructions you can’t set to flush are the legacy x87 opcodes, which you shouldn’t be using in a performance-sensitive context anyway.
Infinity and NaN can also be a performance problem but it depends on your processor. Though of course if you get those during matrix multiplication something has probably gone wrong.
The point of NaNs and infinities is that sometimes it is easier/faster/cleaner to just let those things propagate through the calculation than to do conditional logic that in the end just tends to just simulate the same effect.
True, that consideration is more valid for element-by-element operations rather than things like matrix multiplications. But depending on the operation it might make sense to rely on NaN propagation rather than trying to prescan you matrix for NaNs.
Why not put matrix B in column-major order?
However, while this does improve performance over the naive algorithm, it's still not as good as a tiling algorithm.
That gets the system within a factor of three, which is still embarrassing, but that’s what profiling tools are for. :-)
That said, very well done article. Would be interesting to see this style expand to the more ambitious methods in https://en.wikipedia.org/wiki/Matrix_multiplication_algorith...
BLIS actually tells you how to write a fast production large-matrix GEMM, and the papers linked from https://github.com/flame/blis would be a better reference than the Goto and van de Geijn.
For small matrices see the references from https://github.com/hfp/libxsmm but you're not likely to be re-implementing that unless you're serious about a version on non-x86_64.
Note: Only helpful for specific or very large matrices.
[1]: https://en.wikipedia.org/wiki/Strassen_algorithm [2]: https://en.wikipedia.org/wiki/Coppersmith%E2%80%93Winograd_a...
Matrices have to be truly spectacularly large to benefit from Strassens, and applications of the asymptotically faster algorithms don't really exist. The matrices have to be so large that they're computationally too expensive regardless of the asymptotic speedup. You're talking about dispatching work units to a cluster at that point.
Most importantly though, the discussion is... boring. It's just "yup, we do Strassens, then we quickly reach the point where we go back to the naive algorithm, and things are interesting again."
I don't have personal experience with other mmult methods, but Wikipedia says Strassen's algorithm is at least beneficial at matrix sizes that fit in cache. I have had occasion to deal with matrices of large dimension (though usually sparse, and usually not square) and those numbers don't seem crazy at all. Are they that far off-base? (Wikipedia does say that Coppersmith-Winograd tends to never be a useful improvement.)
In any case, perhaps a more friendly response might be to point out that Strassen's algorithm tends to "bail out" into regular old matrix multiplication for small `n` anyway, so this still helps provide a speedup.
I like simple code as much as the next guy, but occasionally you do need to write fast code.
I wouldn't say that, just look at CGI where a lot of effort is spent making fast 4x4 matrix multiplies. But yeah, a general matrix mult needs to scale, but a lot of the time you know what size you will run and you can optimize for that.
I for one am glad they spend a lot of time thinking about it so that you don't have to and can instead spend your time opining on forums about premature optimization.
I can only judge you by what you say here. Obviously I don't know you, but what other choice do I have? In my experience, those who rely on their tools to do everything for them are useless when things go sideways.
> the point in this case was to learn to write code so that good compilers can output performant SIMD-heavy code
No, the point is that comments like yours serve little purpose. No one is suggesting that you should roll your own lib for matrix math in every situation. However, if you _understand_ what happens behind the scenes you write better code and can more easily diagnose performance issues.
The problem with "don't worry about it" type responses is that sometimes you _do_ end up having to worry about it, and now you're not equipped to solve the problem. Hell, you may not even know that there is a problem to begin with because you have no idea what the runtime characteristics of your algorithm _should_ be.
Maybe you'll have to write some code like this someday. How would you know if your algorithm is performing well? Perhaps you think your 10 hour run is totally acceptible... until someone who read this article and has some experience comes by. They laugh at the absurd performance and spend a bit of time making it 60% faster _because they know how this stuff works_.
There's a lot of value in learning how things work, and "premature optimization" responses like you see all over SO are worthless.
they seem to open new ground in the area you are prusuing edit: if you know any other groups/ppl who practice a similar approach send them my way
anyhow, it seems like the hand written assembly vs compiled code goes in to a new skirmish
the age where the whole spectrum from generalcpu > asic becomes available to programmers all around
bottle necks of parallelism, locality and redundancy seems to be the guiding lights
hand written we'll be probably lead by the magnificent http://www.greenarraychips.com/home/documents/pub/AP001-MD5.... compiler generated by halide and the like
and once we got the ball rolling and more public knowledge is available probably the locked down techniques of the major supercomputers will start to emerge ( i just assume pps from the supercomputers area have been dealing with this stuff for the last couple of decades )
can i spit some more, dear hacker news? focus, you gotta focus, forget about the compiler generating good code, it's all about the ability for us humans to explore the domain
even in halide, they mention the point that you still need to sit down and code different approaches, but their tool makes it a breeze compared to exploring those different approaches using hand written assembly
The correct answer is: don't. Use BLAS (yes, written in Fortran) + OpenMP (if needed), and let the smart people handle this. (Unless you are one of those smart people - in which case, you should improve BLAS, which the rest of us mortals will use).
The longer answer is: matrix multiplication is a highly non-trivial algorithmic problem[1], which is, in fact, still open.
The asymptotically fast algorithms (such as Strassen's[2]) outperform the naive algorithm (i.e. by definition, such is in the article) when matrices get large.
After skimming the article it is still unclear to me how the optimized naive algorithm fares against Strassen's, for example.
The final bit of advice this article is offers is reading the paper on... implementation of BLAS (that's what Goto describes there).
And so that's basically the case where avoiding Goto is considered harmful.
[1]https://en.wikipedia.org/wiki/Matrix_multiplication_algorith...
1. All BLAS implementations I know of don't use Strassen's algorithm.
2. This is such a Stack Overflow answer, please make it stop. Q: "How do I do a thing?" A: "I have unilaterally decided your question is invalid and you should not do a thing." That's really useful!
To the author: hack away. Goto can probably write better ASM but tutorials like this are very helpful for people who'd just like to read some interesting case studies.
If what you are trying to do is to get best performance, make use of work that other people already did, rather than wasting your time on a solved problem.
If you want to learn about how efficient matrix multiplication can be implemented, that is a different problem.
By the way "make use of work that other people already did" might be nice for getting something built fast but it may not be the best thing.
Any issue that involves writing fast code involves tradeoffs. If you sit down to write something new you may have different views on the tradeoffs than whoever wrote "the fastest" one.
Life ain't so black and white. In fact even these 'best of field' products tend to be ugly inside and unoptimized in places. (source: I develop linear algebra libraries)
Bonus: for low level ASM math, every "solved" problem (which by the way it wasn't) becomes unsolved the second Intel or AMD or whoever releases a new chip or coprocessor.
This is something I'm interested in contributing to. Can you name a few libraries (especially ones implementing new and interesting work) that would welcome open source contributors? Alternatively you can just contact me (via my profile info) if you're working on something in particular but would rather not be identified publicly.
SO needs a flag on questions that is set if there is an answer to the actual question as asked and clear otherwise, and that can be used as a search filter.
"On my machine the code runs at around 100 Gflops, which is not far from the theoretical peak, and similar to Intel's optimized implementation."
So he knows that, for example, Intel provides/sells already optimized implementation. And he knows that there is GotoBLAS (1), because as you noted, he cites the paper about it.
But he explains the implementation present in Glow, "a machine learning compiler and execution engine for various hardware targets." (2)
Now if you want to prove to the authors of Glow why they should have "just used BLAS" I would like to read this discussion (or even better, see a proof of concept code with benchmarks etc). But I can imagine that in their case there were some reasons why not to use it, as they were aware that it exists.
However it is true that the readers should be aware of the background assumptions and to be aware of the existence of the libraries.
Those sub-cubic algorithms require the matrix to be very-very-very large to recoup the extra constant costs associated with them.
Wikipedia disagrees:
"In practice, Strassen's algorithm can be implemented to attain better performance than conventional multiplication even for small matrices, for matrices that are not at all square, and without requiring workspace beyond buffers that are already needed for a high-performance conventional multiplication."
Of course, if you only some resources into optimizing the cubic time version, Strassen will look slow by comparison. Better we take a serious look at both and empirically figure out where the trade off point is...
For small, dense n you're not going to beat the cubic algorithm. This is the most common case in a lot of applications.
For sparse n, use a sparse version of the cubic algorithm. For large dense n, those sub-cubic algorithms might be better. Here n would have to be so large that you'd have to factor in the cost of doing it out of core.
Okay so let's have a look at OpenBLAS's Skylake kernel. Maybe we can learn some deep wisdom this author hasn't considered! Go and have a look for it, I'll wait here.
Ah, what's that? THERE IS STILL NO SKYLAKE KERNEL IN OPENBLAS? Interesting.
In fact if you do "sudo apt-get install libopenblas" on Ubuntu 16.04 (the one every deep learning tutorial will be recommending), you'll find you get version 2.18 of OpenBLAS. You'll find that this version of the software is quite old now, and it has a bug that causes it to fail to detect your CPU correctly. You'll find that instead of using any AVX instructions, it'll fall-back to the Prescott kernel. You'll also find this is really fucking slow.
In summary:
a) You have no idea what you're talking about;
b) Even if you were right on these points of detail, your point of view is still terrible
c) Please avoid being so beligerantly wrong in future
While I think there are reasonable arguments for the security of (certain) self-made crypto, taking that security aspect completely aside, we need new people in every field, and cryptography is no exception from that. We should embrace newcomers, not tell me to not do what would teach them so much. Admittingly, one can easily mess up, but then tell how and what to take care of instead of just telling them not to try anything at all.
It seems pretty obvious why you shouldn't design or implement novel cryptography in production without good reason and significant expertise. It also doesn't seem like the people saying this need to spell out that this doesn't preclude learning exercises or legitimate research which won't endanger any real world information. So what's the controversy?
A good example is your comments sibling.
... or, worse yet, they're using crypto implemented by all the people who didn't listen to the advice not to roll their own crypto.
Your comment serves as a poster child for what I'm talking about. Arrogant, looking down on others, deriding their efforts.
Do most hackers have the patience for learning the basics, i.e. some somewhat complicated maths that could take years to learn? No, most of the time it's programmers, and we tend to just start writing code. That's what "don't roll your own crypto" means. Nobody's saying "don't learn about Galois field" or "don't run through cryptopals".
The Dunning-Kruger effect is real. I've seen broken crypto getting shipped. Or people releasing dangerous crypto under "hey, I made this perfectly functional library that even has documentation how to use it, but it's just a learning project". So while that's still happening, I'm fine with hearing "don't roll your own crypto" ad nauseum.
People have every right to learn about and experiment with crypto, but it's just too important to treat as a plaything. If you're marketing your software as "cryptographically secure", you're asking users to place their trust in you to keep their secrets safe. That's a tremendous responsibility and we should treat it with the utmost seriousness. People who should know better keep shipping software with utterly broken DIY crypto, so we need to keep hammering home the message that you should never, ever roll your own crypto in production software.
If someone asked questions on StackOverflow about how to perform brain surgery with kitchen implements, we might want to indulge their curiosity, but we have a moral obligation to say definitely don't do this, because performing brain surgery in your kitchen is an outstandingly terrible idea and someone will probably die. Right now, the software industry is suffering from an epidemic of kitchen brain surgery and it urgently needs to stop.
And I have no need to deride the people who shouldn't be writing crypto. The exploits are factual and speak for themselves.
Such indignant comments routinely get upvoted, but that's an unfortunate weakness of homo internetus, not evidence of a good comment. That's why we have guidelines that people need to follow here. Please (re-)read https://news.ycombinator.com/newsguidelines.html and abide by these rules when posting. They are based on many years of experience and every one is there for good reason.
There's nothing interesting in the lack of Skylake support, as is obvious from the open issue for KNL support. That might even be interesting for the pointers it contains to KNL and SKX GEMM.
OpenBLAS 2.18 certainly does use AVX(2) on appropriate CPUs. If doesn't detect micro-architectures that didn't exist at the time, it hardly a bug, and the correct solution would be to persuade the Ubuntu maintainer to provide the "hardware enablement", or to use BLIS. If tutorials tell you to install "libopenblas" they're wrong because that package doesn't exist.
This is true only if approaching the matrix multiplication problem purely from a theoretical perspective. But the reality is that computers have a specific architecture designed to be as fast as possible, while still respecting the laws of physics.
The libraries such as BLAS deal with those architectures, and it happens that there, an O(n³) triple loop with the optimizations described in the Goto paper is what gives you the fastest performance.
Maybe if we were dealing with 1b*1b matrices, Strassen's like algorithms would start to shine, but with those sizes you already exceed the maximum space on a computer, in which case you then start looking at parallel algorithms on clusters/supercomputers where the algorithms become very different (see things such as SUMMA: Scalable Universal Matrix Multiplication Algorithm). In those cases, the computation happening on each processor would still be the fast O(n³) BLAS code.