Multiplication
From a practical point of view, we need to solve linear algebraic equations using computers and that means listing the basic numerical operations that we need to perform and writing code to implement them.
In practice, there are many operations that are needed. Let's just focus on the most widely applicable operation: matrix multiplication.
Graphically, this is how to multiply two matrices.
\[
A=\begin{pmatrix}
A_{11} & A_{12} & \cdots & A_{1m} \\
A_{21} & A_{22} & \cdots & A_{2m} \\
\vdots & \vdots & \ddots & \vdots \\
A_{n1} & A_{n2} & \cdots & A_{nm} \\
\end{pmatrix}
\qquad
B=\begin{pmatrix}
B_{11} & B_{12} & \cdots & B_{1p} \\
B_{21} & B_{22} & \cdots & B_{2p} \\
\vdots & \vdots & \ddots & \vdots \\
B_{m1} & B_{m2} & \cdots & B_{mp} \\
\end{pmatrix}
\]
\[
(A B)_{ij} = \sum_{k=1}^m A_{ik}B_{kj}
\]
\[
\small
\begin{pmatrix}
\boldsymbol a & \boldsymbol b & \boldsymbol c \\
. & . & . \\
. & . & . \\
\end{pmatrix}
\begin{pmatrix}
\boldsymbol r & . & . \\
\boldsymbol u & . & . \\
\boldsymbol x & . & . \\
\end{pmatrix}
=
\begin{pmatrix}
\boldsymbol \alpha & . & . \\
. & . & . \\
. & . & . \\
\end{pmatrix} \\[25pt]
\begin{pmatrix}
. & . & . \\
\boldsymbol d & \boldsymbol e & \boldsymbol f \\
. & . & . \\
\end{pmatrix}
\begin{pmatrix}
\boldsymbol r & . & . \\
\boldsymbol u & . & . \\
\boldsymbol x & . & . \\
\end{pmatrix}
=
\begin{pmatrix}
. & . & . \\
\boldsymbol \delta & . & . \\
. & . & . \\
\end{pmatrix} \\[25pt]
\begin{pmatrix}
. & . & . \\
\boldsymbol d & \boldsymbol e & \boldsymbol f \\
. & . & . \\
\end{pmatrix}
\begin{pmatrix}
. & \boldsymbol s & . \\
. & \boldsymbol v & . \\
. & \boldsymbol y & . \\
\end{pmatrix}
=
\begin{pmatrix}
. & . & . \\
. & \boldsymbol \epsilon & . \\
. & . & . \\
\end{pmatrix}
\]
Immutable
We're going to create a few implementations that are going to get increasingly further away from canonical Scala. In the remainder of this talk I hope to convince you why you should never ever ever *ever* think that writing your own linear algebra library is a good idea.
Immutable. This was mostly because I wanted to see how bad it was. Although this makes perfect sense from a functional programming point of view, it is ridiculous to consider writing anything like this in a numerical programming world.
We at least have the sense to use a 2D mutable array internally when performing the multiplication.
case class ImmutableMatrix (
numRows: Int,
numCols: Int,
data: Vector[Vector[Double]]
) {
def mult (that: ImmutableMatrix): ImmutableMatrix = {
require (numCols == that.numRows )
val res = Array.fill (numRows, that.numCols )(0.0 )
for {
i <- 0 until numRows
j <- 0 until that.numCols
k <- 0 until numCols
} {
res (i)(j) = data (i)(k) * that.data (k)(j)
}
ImmutableMatrix (numRows, numCols, ImmutableMatrix.arrayToVec (res))
}
}
Naive
trait Matrix {
def numRows: Int
def numCols: Int
def set (row: Int, col: Int, value: Double): Unit
def get (row: Int, col: Int): Double
def mult (that: Matrix): Matrix
}
class NaiveMatrix (
val numRows: Int,
val numCols: Int,
val values: Array[Array[Double]]
) extends Matrix {
def set (row: Int, col: Int, value: Double): Unit =
values (row)(col) = value
def get (row: Int, col: Int): Double = values (row)(col)
def mult (that: Matrix): Matrix = {
val res = NaiveMatrix (numRows, that.numCols )
for {
i <- 0 until numRows
j <- 0 until that.numCols
k <- 0 until numCols
} {
val update = this .get (i, k) * that.get (k, j)
res.set (i, j, res.get (i, j) + update)
}
res
}
}
Naive Parallel
We can even see the obvious possibility for parallelisation.
I've even seen people do this with Akka Actors, believe it or not.
trait NaiveParallelMatrix {
this : NaiveMatrix =>
override def mult (that: Matrix): Matrix = {
val res = NaiveMatrix (numRows, that.numCols )
for {
i <- (0 until numRows).par
j <- (0 until that.numCols ).par
k <- 0 until numCols
} {
val update = this .get (i, k) * that.get (k, j)
res.set (i, j, res.get (i, j) + update)
}
res
}
}
Allocation
But this is still *very* naive. One problem is that we're creating a load of objects and the garbage collector is doing a lot of work, causing context switches in the CPU and generally slowing things down.
How do we know that there are loads of objects being created? We can use Lion's Share to see that.
http://github.com/fommil/lions-share
While Loops
We can avoid all of that memory allocation by falling back to a basic while loop with mutable counters.
You can even get a slight difference in performance by introducing a sum
counter.
trait NaiveWhileMatrix {
this : NaiveMatrix =>
override def mult (that: Matrix): Matrix = {
val res = NaiveMatrix (numRows, that.numCols )
var i, j, k = 0
while (i < numRows) {
j = 0
while (j < that.numCols ) {
k = 0
var sum = 0.0
while (k < numCols) {
sum += this .get (i, k) * that.get (k, j)
k += 1
}
res.set (i, j, sum)
j += 1
}
i += 1
}
res
}
}
These are performance results for all the implementations we've seen so far. The x
axis is the number of rows or columns for a square matrix and the y
axis is time. This is a logarithmic graph, so every major tick is an order of magnitude difference in performance.
The point here is to demonstrate the chasm in numerical performance between primitive data structures and higher level concepts that we, as Scala developers, have perhaps become used to.
The while loop is not dissimilar to Java matrix libraries that you may have heard of, such as COLT, which was developed at CERN over 10 years ago.
But this is what I consider to be lukewarm performance.
netlib.org
BLAS
Source: http://www.netlib.org/blas/blasqr.pdf
BLAS, short for "Basic Linear Algebra Subprograms" is a Fortran API split into three levels.
Level 1 contains vector operations on arrays: dot products, vector norms, a generalized vector addition of this form.
Level 2 was started in 1984 and published in 1988, containing matrix-vector operations including a generalized matrix-vector multiplication (GEMV), as well as a solver for \(x\) when \(T\) is triangular. The Level 2 subroutines are especially intended to improve performance of programs using BLAS on vector processors, where Level 1 BLAS are suboptimal because they hide the matrix-vector nature of the operations from the compiler.
Level 3 was formally published in 1990 and contains matrix-matrix operations, including a "general matrix multiplication" (GEMM), where A and B can optionally be transposed inside the routine. Also included are routines for solving B for triangular matrices T.
At least we now know the extent of our naivety: we tried to re-implement DGEMM
.
1
xAXPY
\(y \leftarrow \alpha x + y\)
1
xDOT
\(dot \leftarrow x^T y\)
1
...
2
xGEMV
\(y \leftarrow \alpha A x + \beta y\)
2
xTRSV
\(T \boldsymbol x = y\)
2
...
3
xGEMM
\(C \leftarrow \alpha A B + \beta C\)
3
xTRSM
\(T \boldsymbol B = \alpha \boldsymbol B\)
3
...
Reference DGEMM
BLAS is only an API, but there is a reference implementation which is maintained to this day and is probably installed on your Linux boxen.
This is a snippet from one of the four branches of the reference implementation of DGEMM
, after input parameter checks.
It turns out that we weren't so naive afterall. It's basically doing the same thing that we were doing, except `DGEMM` needs to take these extra `ALPHA` and `BETA` parameters to be more general.
But it's close to the metal, and the compiler will be able to do magic with this, right? We'll see about that later.
* Form C := alpha* A* B + beta* C
DO 90 J = 1 ,N
IF (BETA.EQ. ZERO) THEN
DO 50 I = 1 ,M
C(I,J) = ZERO
50 CONTINUE
ELSE IF (BETA.NE. ONE) THEN
DO 60 I = 1 ,M
C(I,J) = BETA* C(I,J)
60 CONTINUE
END IF
DO 80 L = 1 ,K
IF (B(L,J).NE. ZERO) THEN
TEMP = ALPHA* B(L,J)
DO 70 I = 1 ,M
C(I,J) = C(I,J) + TEMP* A(I,L)
70 CONTINUE
END IF
80 CONTINUE
90 CONTINUE
LAPACK
Short for Linear Algebra PACKage. It is basically a big collection of canonical implementations of mathematical algorithms for solving linear algebra systems.
LAPACK is still under active development and version 3.5.0 was released a year ago.
LAPACK primarily provides solvers and specialist multiplication routines for 28 different types of matrices: that is matrices with different a-priori structure or properties.
It's far too big for any kind of short summary to make any sense, so I'm just intimidating you with the list of routines available for double precision calculations.
dbdsdc dbdsqr ddisna dgbbrd dgbcon dgbequ dgbequb dgbrfs dgbrfsx dgbsv
dgbsvx dgbsvxx dgbtrf dgbtrs dgebak dgebal dgebrd dgecon dgeequ
dgeequb dgees dgeesx dgeev dgeevx dgehrd dgejsv dgelqf dgels dgelsd
dgelss dgelsy dgeqlf dgeqp3 dgeqpf dgeqrf dgeqrfp dgerfs dgerfsx
dgerqf dgesdd dgesv dgesvd dgesvj dgesvx dgesvxx dgetrf dgetri dgetrs
dggbak dggbal dgges dggesx dggev dggevx dggglm dgghrd dgglse dggqrf
dggrqf dggsvd dggsvp dgtcon dgtrfs dgtsv dgtsvx dgttrf dgttrs dhgeqz
dhsein dhseqr dopgtr dopmtr dorgbr dorghr dorglq dorgql dorgqr dorgrq
dorgtr dormbr dormhr dormlq dormql dormqr dormrq dormrz dormtr dpbcon
dpbequ dpbrfs dpbstf dpbsv dpbsvx dpbtrf dpbtrs dpftrf dpftri dpftrs
dpocon dpoequ dpoequb dporfs dporfsx dposv dposvx dposvxx dpotrf
dpotri dpotrs dppcon dppequ dpprfs dppsv dppsvx dpptrf dpptri dpptrs
dpstrf dptcon dpteqr dptrfs dptsv dptsvx dpttrf dpttrs dsbev dsbevd
dsbevx dsbgst dsbgv dsbgvd dsbgvx dsbtrd dsfrk dspcon dspev dspevd
dspevx dspgst dspgv dspgvd dspgvx dsprfs dspsv dspsvx dsptrd dsptrf
dsptri dsptrs dstebz dstedc dstegr dstein dstemr dsteqr dsterf dstev
dstevd dstevr dstevx dsycon dsyequb dsyev dsyevd dsyevr dsyevx dsygst
dsygv dsygvd dsygvx dsyrfs dsyrfsx dsysv dsysvx dsysvxx dsytrd dsytrf
dsytri dsytrs dtbcon dtbrfs dtbtrs dtfsm dtftri dtfttp dtfttr dtgevc
dtgexc dtgsen dtgsja dtgsna dtgsyl dtpcon dtprfs dtptri dtptrs dtpttf
dtpttr dtrcon dtrevc dtrexc dtrrfs dtrsen dtrsna dtrsyl dtrtri dtrtrs
dtrttf dtrttp dtzrzf dsgesv dsposv
DGETRF
Let's just look at one of those methods, as an example of the amount of thought that has gone into this library. Take DGETRF
, it computes the LU Decomposition of a matrix.
The LU Decomposition breaks a matrix into three, simpler, matrices. The Lower, the Upper and a diagonal matrix The theory behind why this is possible is absolutely beautiful and if you're interested further I strongly recommend Topics in Algebra by Herstein.
This is a graphical representation of the LU decomposition of a Walsh matrix, which has some applications in error correcting codes. So, DGETRF
is able to produce L, U and P given an A.
Matrices of this L, U or P form are known as structured sparse, because their structure is known. From a practical point of view, the memory requirements are much lower than storing a full matrix. From a problem solver's point of view, some equations are known to be solvable when the matrices have particular forms.
\[
LP^{-1}U = A
\]
Optional: Solving with LU
OPTIONAL: This is an optional mathematics slide, with an example of where LU decomposition can be used to solve a problem. Is there any interest in seeing this, or do you all know it already?
If we have a linear system \(Ax = b\) where \(A\) is a matrix, and \(x,b\) are vectors, we'd like to calculate \(x\) given \(A\) and \(b\) .
We can calculate the LU Decomposition of \(A\) , remembering that \(P\) is a diagonal matrix, and rearrange like so.
Then we solve for an intermediate vector \(y\) , before solving for \(x\) . The great thing about these two equations is that \(L\) and \(U\) are both triangular matrices, so we can use forward and back substitution like many of you may have learnt at high school.
Once the LU Decomposition has been calculated, many systems of this form can be solved efficiently.
\(Ax=b\)
\(PA = LU\)
\(LUx = Pb\)
\(Ly = Pb\)
\(Ux = y\)
This is how forward substitution works for lower triangular matrices. Back substitution is the same thing, but for upper triangular matrices.
Observe that the first equation only involves \(x_1\) , and thus one can solve for \(x_1\) directly. The second equation only involves \(x_1\) and \(x_2\) , and thus can be solved once one substitutes in the already solved value for x1 . Continuing in this way, everything can be deduced.
\[
\begin{matrix}
l_{1,1} x_1 & & & & & = & b_1 \\
l_{2,1} x_1 & + & l_{2,2} x_2 & & & = & b_2 \\
\vdots & & \vdots & \ddots & & & \vdots \\
l_{m,1} x_1 & + & l_{m,2} x_2 & + \dotsb + & l_{m,m} x_m & = & b_m \\
\end{matrix}
\]
\[
x_1 = \frac{b_1}{l_{1,1}} \\
x_2 = \frac{b_2 - l_{2,1} x_1}{l_{2,2}} \\
x_m = \frac{b_m - \sum_{i=1}^{m-1} l_{m,i}x_i}{l_{m,m}} \\
\]
C API
In the GNU toolchain, Fortran's single-precision type maps into C float
and double precision into double
, which are consistent with the Java and Scala primitives of the same name.
But accessing the Fortran API from C is really nasty: there isn't even an official header file for blas.h
so you don't even get parameter list checking. Instead, BLAS is accessed through the CBLAS API and LAPACK is accessed through LAPACKE. This allows C constructs to be used instead of magic characters. This is an example of what the CBLAS wrapper is doing.
It's worth noting that in Fortran, all input parameters are pointers. This is in contrast to C and Java where primitive types are copied when they are passed to a function. In Fortran, you pass a pointer to the primitive and it is always possible to update that primitive within your routine. Even more mutable than C and Java, what can go wrong?
#include "cblas.h"
void cblas_dgemm(const enum CBLAS_ORDER Order, const enum CBLAS_TRANSPOSE TransA,
const enum CBLAS_TRANSPOSE TransB, const int M, const int N,
const int K, const double alpha, const double *A,
const int lda, const double *B, const int ldb,
const double beta, double *C, const int ldc) {
...
if ( Order == CblasColMajor ) {
if (TransA == CblasTrans) TA='T';
else if ( TransA == CblasConjTrans ) TA='C';
else if ( TransA == CblasNoTrans ) TA='N';
else ...
if (TransB == CblasTrans) TB='T';
else if ( TransB == CblasConjTrans ) TB='C';
else if ( TransB == CblasNoTrans ) TB='N';
else ...
#ifdef F77_CHAR
F77_TA = C2F_CHAR(&TA);
F77_TB = C2F_CHAR(&TB);
#endif
F77_dgemm(F77_TA, F77_TB, &F77_M, &F77_N, &F77_K, &alpha, A,
&F77_lda, B, &F77_ldb, &beta, C, &F77_ldc);
} else if (Order == CblasRowMajor) { ...
Kinds of Error
Source: http://www.netlib.org/lapack/lug/node72.html
There are two kinds of numerical error.
Roundoff error arises from rounding results of floating-point operations during the algorithm. Input error is error in the input to the algorithm from prior calculations or measurements.
Both are measured in multiples of machine precision, loosely defined as the largest relative error in any floating-point operation that neither overflows nor underflows. (Overflow means the result is too large to represent accurately, and underflow means the result is too small to represent accurately.
roundoff error
input error
machine precision: \(\epsilon\)
Types of Structures
We're almost getting back to Scala, we now have Scalars
LAPACK routines return four types of floating-point output arguments.
Scalar , numbers, e.g. an eigenvalue
Vector , e.g. the solution \(x\) of system \(Ax=b\)
Matrix , e.g. matrix inverse \(A^{-1}\)
Subspace , e.g. space spanned by eigenvectors
Relative Errors
First consider scalars. Let the scalar \(\hat{\alpha}\) be an approximation of the true answer \(\alpha\) . We can measure the difference between \(\alpha\) and \(\hat{\alpha}\) either by the absolute error \(\vert \hat{\alpha} - \alpha \vert\) , or, if \(\alpha\) is nonzero, by the relative error \(\vert \hat{\alpha} - \alpha \vert / \vert
\alpha \vert\) . Alternatively, it is sometimes more convenient to use
\(\vert \hat{\alpha} - \alpha \vert / \vert \hat{\alpha} \vert\) instead.
If the relative error of \(\hat{\alpha}\) is, say \(10^{-5}\) , then we say that \(\hat{\alpha}\) is accurate to 5 decimal digits.
\(\hat{\alpha} \approx \alpha\)
\(\vert \hat{\alpha} - \alpha \vert\)
\(\vert \hat{\alpha} - \alpha \vert / \vert \alpha \vert\)
\(\vert \hat{\alpha} - \alpha \vert / \vert \hat{\alpha} \vert\)
\(10^{-5}\epsilon\) "5 decimal digits"
Norms
In order to measure the error in vectors and matrices, we need to measure their size, or more correctly its *norm* .
A simple norm to understand is the 1-norm, which simply adds up all the elements of a vector, or the largest column vector in a matrix.
The 2-norm is perhaps more intuitive for vectors, as it is the Euclidean metric, or Pythagoras. The equivalent for matrices is more complicated - and carries a non-trivial computational cost - so the Frobenius norm is usually substituted, although not at negligible cost.
The simplest of all norms is the magnitude of the largest component, the "infinity norm", which is understandable for vectors and matrices alike.
Thinking geometrically, the 1-norm is like considering the distance between two points in a city to be the distance that a taxi would drive between them on the city's traffic grid. The 2-norm would be "as the crow flies" and the infinity norm is like just remembering how far you have come North, South, East or West. However, such analogies can be misleading because there is nothing particularly special about the 2-norm that makes it universally more accurate.
1-norm
\(\Vert x\Vert _{1} = \sum_i \vert x_i\vert\)
\(\Vert A\Vert _{1} = \max_j \sum_i \vert a_{ij}\vert\)
2-norm
\(\Vert x\Vert _2 = ( \sum_i \vert x_i\vert^2 )^{1/2}\)
\(\Vert A\Vert _2 = \max_{x \neq 0} \Vert Ax\Vert _2 / \Vert x\Vert _2\)
Frobenius
\(\Vert x \Vert_F = \Vert x \Vert_2\)
\(\Vert A\Vert _F = ( \sum_{ij} \vert a_{ij}\vert^2 )^{1/2}\)
\(\infty\) norm
\(\Vert x\Vert _{\infty} = \max_i \vert x_i\vert\)
\(\Vert A\Vert _{\infty} = \max_i \sum_j \vert a_{ij}\vert\)
Condition Number
The condition number of a matrix A is defined as \(\kappa_p (A) \equiv
\Vert A\Vert _p \cdot \Vert A^{-1}\Vert _p\) , where A is square and invertible, and the norm is computed according to the previous slide.
The condition number measures how sensitive \(A^{-1}\) is to changes in \(A\) ; the larger the condition number, the more sensitive is \(A^{-1}\) . This has consequences for backwards stability. Have you ever typed a phrase into Google Translate and then translated it back into English? Its the same sort of thing here, and the more you do it the weirder it gets.
LAPACK routines will typically report the approximate reciprocal of the condition number.
\[
A = \left(
\begin{array}{ccc}
1 & 2 & 3 \\
4 & 5 & 6 \\
7 & 8 & 10
\end{array} \right)
\qquad
A^{-1} \approx \left(
\begin{array}{ccc}
-.667 & -1.333 & 1 \\
-.667 & 3.667 & -2 \\
1 & -2 & 1
\end{array} \right)
\]
\[\kappa_{\infty}(A) = 158.33\]
LAPACK RCOND(A)
\(\approx \frac 1 {\kappa_\infty}\)
Example
Suppose we want to solve \(Ax=b\) . We can calculate the error in our solution for \(x\) by querying netlib for the norm of \(A\) with SLANGE
and the reciprocal of \(A\) with SGECON
, multiplying by the machine error to get a bound on the error. We cheat and know the actual error, shown here for comparison.
But netlib also provides "expert" solvers, or drivers, which are much more accurate and can report on their error bound. Here, SGESVX
, the expert equivalent, returns a more accurate solution with a smaller error bound.
There is ongoing research in the netlib community to come up with ever-more accurate solvers, and the main point I wanted to get across here was that this is something that is often overlooked by amateur implementations, but is absolutely critical to a wide range of applications. Not knowing your error bars can be disastrous.
\[
Ax = b \\
A = \left( \begin{array}{ccc}
4 & 16000 & 17000 \\
2 & 5 & 8 \\
3 & 6 & 10
\end{array}\right) \qquad
b = \left( \begin{array}{c}
100.1 \\
.1 \\
.01
\end{array}\right)
\]
ANORM = SLANGE( 'I' , N, N, A, LDA, WORK )
CALL SGESV( N, 1 , A, LDA, IPIV, B, LDB, INFO )
CALL SGECON( 'I' , N, A, LDA, ANORM, RCOND, WORK, IWORK, INFO )
ERRBD = EPSMCH / RCOND
SGESV ERRBD
\(= 1.5\cdot 10^{-2}\)
true error \(= 1.5\cdot 10^{-3}\)
SGESVX FERR
\(= 3.0 \cdot 10^{-5}\)
true error \(4.3 \cdot 10^{-7}\)
Hardware Support
Not only are algorithms and computational techniques always improving, but the hardware also gives incredible benefits.
In particular, Intel have been adding a range of multiply accumulate operations to their latest ranges of CPUs which allow operations like multiplication of two numbers and an addition to occur in one clock cycle and with a single unit of machine rounding.
\(y = a * x + b\)
VFMADD132SD
xmm,xmm,xmm/m64
$0=$0×$2+$1
VFMADD213SD
xmm,xmm,xmm/m64
$0=$1×$0+$2
VFMADD231SD
xmm,xmm,xmm/m64
$0=$1×$2+$0
Intel
You'll see in a moment, it's wipes the floor but really this slide summarises a practical consideration about the Intel implementation of BLAS.
This is a proprietary product, so we have no idea how they are doing anything.
It comes as part of a monolithic corporate package called Parallel Studio XE along with a bunch of other stuff that you probably don't need. And this is the price list as of November 2014.
The license does allow you to redistribute with your software, but you're looking at cluster licenses for every CI/QA/PROD box and either professional or composer licenses on DEV boxes.
Renewal prices in brackets.
However, if you speak to someone in Intel who knows about the products, they'll tell you that you can still buy the MKL standalone and it is a much more reasonable price. Honestly, if you do anything high performance, I think it's worth it. There is a tonne of other good stuff.
Parallel Studio
Named User
$2,949 ($1,049)
$2,299 ($799)
$1,449 ($499)
2 Floating Users
$14,749 ($5,199)
$11,499 ($4,049)
$5,099 ($1,799)
5 Floating Users
$29,499 ($10,349)
$22,999 ($8,049)
$10,899 ($3,849)
MKL Standalone
$499
AMD Core Math Library
In contrast, AMD have a gratis library called ACML. It's not free software, but you can download without cost and redistribute.
And because it is proprietary, we have no idea how they are achieving their results.
I actually don't have access to any AMD hardware, but it is possible to run this library on Intel hardware, which is interesting. They only have a version for Linux and Windows.
Apple vecLib / Accelerate
Apple have their own machine optimised distribution of both BLAS and LAPACK, which is incredibly convenient.
$ ll /usr/lib/lib{blas, lapack}.dylib
/usr/lib/libblas.dylib -> /System/Library/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
/usr/lib/liblapack.dylib -> /System/Library/Frameworks/vecLib.framework/Versions/A/libLAPACK.dylib
$ tree /System/Library/Frameworks/vecLib.framework/Versions/Current/
├── _CodeSignature
│ └── CodeResources
├── libBLAS.dylib
├── libLAPACK.dylib
├── libvDSP.dylib
├── libvMisc.dylib
├── Resources
│ ├── English.lproj
│ │ └── InfoPlist.strings
│ ├── Info.plist
│ └── version.plist
└── vecLib
ATLAS
The most well-known open source implementation of BLAS is ATLAS: Automatically Tuned Linear Algebra Software.
ATLAS relies heavily on compile-time analysis, which they call "automated empirical optimisation of software": ironically something can be solved using linear algebra. This means three main things, all involving compiling ATLAS lots of times:
compiling the same code with different compiler flags to see if there are any free wins.
compiling the same code with lots of different magic numbers to define things like cache edges and whether or not to copy matrices of various sizes.
trying out various different implementations of the same function, where there is no clear winner.
code generation of the level 3 BLAS and LAPACK routines to take all the above into account at the level of branches.
compiler flags - free wins
parameterisation
cache edges - how much to attempt in kernels?
copy - matrix data can be rearranged optimally
multiple implementations of the same function
Fred's loop is faster than Bob's on your machine
code generation
But the ATLAS binaries will have been optimised on somebody else's machine with a different spec, so to really get maximum performance you need to compile this yourself. For debian based systems, it's very straightforward -- just make sure you don't use your computer for anything else while it's working.
Personally, I've never seen a big win by building on my machine, but I strongly suspect that the Debian build machines have the same CPU as me. Your mileage may vary. It's free, to do this, so why not?
cat /usr/share/doc/libatlas3-base/README.Debian
for A in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do sudo sh -c "echo performance > $A"
done
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
sudo apt-get source atlas
sudo apt-get build-dep atlas
sudo apt-get install devscripts
cd atlas-*
fakeroot debian/rules custom
sudo dpkg -i ../libatlas*.deb
sudo update-alternatives --config libblas.so.3
sudo update-alternatives --config liblapack.so.3
GotoBLAS / OpenBLAS
Another free and open source library.
GotoBLAS was created by Kazushige Goto in 2003 during his sabbatical from the Japan Patent office and the design concept is to write fast assembly for known processors.
Development ceased in 2008, and at its peak it was used in 7 of the world's top 10 supercomputers.
However, OpenBLAS took over and expands the list of processors that it supports to the i7.
The big advantage of OpenBLAS is that it doesn't require tuning on the target machine, but there is always the risk that some functions aren't as fast as they could be due to the lack of tuning.
$ ls OpenBLAS/kernel/x86_64/*dgemm*
dgemm_kernel_16x2_haswell.S dgemm_kernel_6x4_piledriver.S
dgemm_ncopy_2.S dgemm_ncopy_8.S dgemm_tcopy_8_bulldozer.S
dgemm_kernel_4x4_haswell.S dgemm_kernel_8x2_bulldozer.S
dgemm_ncopy_4.S dgemm_tcopy_2.S dgemm_tcopy_8.S
dgemm_kernel_4x8_sandy.S dgemm_kernel_8x2_piledriver.S
dgemm_ncopy_8_bulldozer.S dgemm_tcopy_4.S
DGEMM Benchmarks
requestStart = currentTimeNanos();
double * c = calloc(m * m, sizeof (double ));
cblas_dgemm(CblasColMajor, CblasNoTrans, CblasNoTrans, m, m, m, 1 , a, m, b, m, 0 , c, m);
requestEnd = currentTimeNanos();
LD_LIBRARY_PATH=. ./dgemmtest > ../R/netlib-ref.csv
LD_LIBRARY_PATH=/usr/lib/atlas-base ./dgemmtest > ../R/atlas.csv
LD_LIBRARY_PATH=/usr/lib/openblas-base ./dgemmtest > ../R/openblas.csv
LD_LIBRARY_PATH=/opt/acml5.3.1/gfortran64_mp/lib/:. ./dgemmtest > ../R/acml.csv
LD_LIBRARY_PATH=/opt/intel/mkl/lib/intel64:. ./dgemmtest > ../R/intel.csv
Here is the chart you've all been waiting for, overlaid on top of our Scala implementations. There is a lot of information here, so let's go through it slowly.
The Scala implementations are the black lines, same as before.
The red lines mark out the High Performance Zone that we discussed in the Bottleneck section.
The thick green line is the fortran reference implementation of blas. So the only real difference between this and the bottleneck discovery code is that it actually does the multiplications. We can really see that the time to do the multiplications is almost insignificant, as this is effectively sitting on the fence.
Let's then go into the open source implementations in green: ATLAS is good but OpenBLAS is leading the field.
We also get real performance gains from the proprietary implementations: AMD is in thick blue and Intel is below with blue dashes, and somewhere in between is the Apple implementation. Even though I run these a few times to allow warm-up, the proprietary implementations seem to have these spikes. I have no idea what they are: possibly poor handling of cases on cache boundaries.
It is truly remarkable that the optimised implementations take only a single order of magnitude more to do the work than to simply copy the arrays. And that's about a thousand times faster than our first attempt at a Scala implementation, and almost 100 times faster than the best we could come up with on the JVM.
And this is the same graph on a machine with 16 Intel(R) Xeon(R) CPU E5-2643 0 @ 3.30GHz cores. No weird spikes from the proprietary implementations, again I don't know why.
It should be noted that this is only DGEMM
, for any specific problem it may be best to compare all the functions that matter with the correct transpose and matrix ordering modes.
Fortran to ...
netlib-java begins with a direct translation of the netlib reference implementation into Java, using a tool called F2J.
The tool is written in C++ and takes Fortran, such as this part of DSYMM
used for multiplying symmetric matrices:
IF ( UPPER )THEN
DO 70 , J = 1 , N
DO 60 , I = 1 , M
TEMP1 = ALPHA* B( I, J )
TEMP2 = ZERO
DO 50 , K = 1 , I - 1
C( K, J ) = C( K, J ) + TEMP1 * A( K, I )
TEMP2 = TEMP2 + B( K, J )* A( K, I )
50 CONTINUE
IF ( BETA.EQ. ZERO )THEN
C( I, J ) = TEMP1* A( I, I ) + ALPHA* TEMP2
ELSE
C( I, J ) = BETA * C( I, J ) +
$ TEMP1* A( I, I ) + ALPHA* TEMP2
END IF
60 CONTINUE
70 CONTINUE
... Java
Into Java, such as this.
There are a few things of particular note here. In Fortran, arrays are indexed from ONE --- as God intended. But obviously, in C and Java, the indexes start from zero, so there is a lot of plus/minus one going on.
The other thing is that Fortran likes to pass around pointers to parts of the array, but there is no way on the JVM to pass a reference to the middle of an array. You have to pass the full array and an offset/sublength. So that means there is a lot of boilerplate to deal with this.
Finally, Fortran 77 doesn't have loops and instead uses GOTO
statements. For simple examples like this, loops are inferred and used, but in other parts of the code it is simply not possible to implement in Java, so Dummy.label
is added at the various target points and a special Java processor is applied to construct JVM bytecode.
In fact, the production of the Java code itself is really a lie because the preferred behaviour of F2J is to compile straight to JVM bytecode.
if (upper) {
forloop70: for (j = 1 ; j <= n; j++) {
forloop60: for (i = 1 ; i <= m; i++) {
temp1 = alpha * b[(i) - 1 + (j - 1 ) * ldb + _b_offset];
temp2 = zero;
forloop50: for (k = 1 ; k <= i - 1 ; k++) {
c[(k) - 1 + (j - 1 ) * Ldc + _c_offset] = c[(k) - 1 + (j - 1 ) * Ldc + _c_offset] + temp1 * a[(k) - 1 + (i - 1 ) * lda + _a_offset];
temp2 = temp2 + b[(k) - 1 + (j - 1 ) * ldb + _b_offset] * a[(k) - 1 + (i - 1 ) * lda + _a_offset];
Dummy.label ("Dsymm" , 50 );
}
if (beta == zero) {
c[(i) - 1 + (j - 1 ) * Ldc + _c_offset] = temp1 * a[(i) - 1 + (i - 1 ) * lda + _a_offset] + alpha * temp2;
} else {
c[(i) - 1 + (j - 1 ) * Ldc + _c_offset] = beta * c[(i) - 1 + (j - 1 ) * Ldc + _c_offset] + temp1 * a[(i) - 1 + (i - 1 ) * lda + _a_offset] + alpha * temp2;
}
Dummy.label ("Dsymm" , 60 );
}
Dummy.label ("Dsymm" , 70 );
}
}
API Generator
Once we have the entire netlib reference implementation in jar
form, netlib-java is able to parse all the objects and construct an abstract interface for both BLAS and LAPACK.
In fact, pretty much all of netlib-java is code generation, and it's using the ANTLR StringTemplate library to do that.
This is the sort of thing you can expect in the interface.
abstract class BLAS {
...
/**
* ... original docs here ...
*/
abstract public void dgemm (
String transa,
String transb,
int m,
int n,
int k,
double alpha,
double [] a,
int lda,
double [] b,
int ldb,
double beta,
double [] c,
int Ldc
);
...
}
Regex Digression
It's worth noting that in order to work out the names of the parameters to the routines, I had to parse the Javadocs that F2J produces, because this sort of information is not present in the Java 6 class definition. I humbly apologise for having unleashed this regular expression upon your dependency list:
StringBuilder regex = new StringBuilder();
regex.append (format ("> \\ Q%s \\ E</A></(?:B|strong)> \\ (" , name));
for (Class klass : types) {
regex.append (format (
",? \\ s*(?:<A[^>]+>)?[ \\ w.]* \\ Q%s \\ E(?:</A>)?(?:<[^&]+>)? ([^), \\ s]+)" ,
klass.getSimpleName ()
));
}
regex.append (format (" \\ )</CODE>" ));
Pattern pattern = Pattern.compile (regex.toString (), MULTILINE | CASE_INSENSITIVE);
Matcher matcher = pattern.matcher (raw);
Implementations
The abstract API is implemented by three different implementations, looking something like this.
For the native implementation, this means passing off to JNI.
F2jBLAS
@Override
public void dgemm (
String transa, String transb, int m, int n, int k,
double alpha, double [] a, int lda,
double [] b, int ldb, double beta, double [] c, int Ldc
) {
org.netlib .blas .Dgemm .dgemm (
transa, transb, m, n, k, alpha, a, 0 , lda, b, 0 , ldb, beta, c, 0 , Ldc
);
}
NativeRefBLAS and NativeSystemBLAS
@Override
public native void dgemm (
String transa, String transb, int m, int n, int k,
double alpha, double [] a, int lda,
double [] b, int ldb, double beta,
double [] c, int Ldc
);
JNI
The Java Native Interface (JNI) implementation, which is auto-generated, looks something like this. I'd like to talk about some pitfalls with JNI.
There is a really nice third-party wrapper around native functions called JNA, but it has a major performance problem: it copies arrays both in and out of the native layer. And we know that's not great for performance.
But if we use standard JNI functions, and ask for ArrayPrimitive
we'll also get a copy of the array. In order for the JNI code to be handed the actual array, as used in the JVM, we have to call CriticalArrayPrimitive
.
But there is a problem with CriticalArrayPrimitive
, once you call it, you potentially turn off the garbage collector. That could be a big problem if you are in a churn-heavy application and you're doing many of these operations.
But it's not as bad as it sounds. The reality is that the optimised native implementations make full use of your CPU, so there isn't much scope to do more than one of them at a time. Therefore, you're well advised to perform your higher level matrix operations in an Akka actor or in a synchronized block, to let the JVM have a bit of breathing room on either side to collect any garbage. This native code doesn't actually produce any garbage, but your code may do that while it's running.
If we wanted to avoid this problem, we could use NIO Buffers
. But the problem with that is that it doesn't match onto the F2J implementation, so everyone living in pure JVM land will take the hit. Ideally, if I had an infinite amount of time to work on this, I'd rewrite F2J to use Buffers
instead of arrays.
There are also some nasty implementation details to do with the exact size of primitives between C and JVM land, which means that not everything matches up nicely. Were you aware that the JVM represents int
with a 64 bit C long long
type? I've even seen people write --- shocking --- code in Scala to try and optimise heap usage, completely unaware of this fact.
JNIEXPORT void JNICALL Java_com_github_fommil_netlib_NativeSystemBLAS_dgemm (JNIEnv * env, jobject calling_obj, jstring transa, jstring transb, jint m, jint n, jint k, jdouble alpha, jdoubleArray a, jint lda, jdoubleArray b, jint ldb, jdouble beta, jdoubleArray c, jint Ldc) {
char * jni_transa = (char *)(*env)->GetStringUTFChars(env, transa, JNI_FALSE);
char * jni_transb = (char *)(*env)->GetStringUTFChars(env, transb, JNI_FALSE);
jdouble * jni_a = NULL;
if (a != NULL) {
jni_a = (*env)->GetPrimitiveArrayCritical(env, a, JNI_FALSE);
check_memory(env, jni_a);
}
jdouble * jni_b = NULL;
if (b != NULL) {
jni_b = (*env)->GetPrimitiveArrayCritical(env, b, JNI_FALSE);
check_memory(env, jni_b);
}
jdouble * jni_c = NULL;
if (c != NULL) {
jni_c = (*env)->GetPrimitiveArrayCritical(env, c, JNI_FALSE);
check_memory(env, jni_c);
}
cblas_dgemm(CblasColMajor, getCblasTrans(jni_transa), getCblasTrans(jni_transb), m, n, k, alpha, jni_a, lda, jni_b, ldb, beta, jni_c, Ldc);
if (c != NULL) (*env)->ReleasePrimitiveArrayCritical(env, c, jni_c, 0 );
if (b != NULL) (*env)->ReleasePrimitiveArrayCritical(env, b, jni_b, 0 );
if (a != NULL) (*env)->ReleasePrimitiveArrayCritical(env, a, jni_a, 0 );
(*env)->ReleaseStringUTFChars(env, transb, jni_transb);
(*env)->ReleaseStringUTFChars(env, transa, jni_transa);
}
Maven Native
A real pain with natives is actually building them. It's possible to use maven's native-maven-plugin
to build natives but quite a lot of work is needed to get all the platforms to build on one machine.
Unfortunately, I was unable to get OS X binaries to cross compile from Linux, so I have to use two build machines when I create a release. I'd love to fix that, so if anyone can help me to cross compile Fortran code on Linux targeted at OS X, please get in contact.
<plugin>
<groupId> org.codehaus.mojo</groupId>
<artifactId> native-maven-plugin</artifactId>
<configuration>
<javahVerbose> true</javahVerbose>
<javahClassNames>
<javahClassName> com.github.fommil.netlib.NativeSystemBLAS</javahClassName>
</javahClassNames>
<compilerStartOptions>
<compilerStartOption> -O3</compilerStartOption>
</compilerStartOptions>
<sources>
<source>
<directory> ${project.build.directory}/netlib-native</directory>
<includes>
<include> *.c</include>
</includes>
...
JNILoader
The other big problem with distributing binaries is how to actually load them. The Java Standard library doesn't exactly make it easy, so I had to write JNILoader
which allows natives to be bundled as resources which are then unpackaged at runtime into a temporary directory, with safe handling that will throw a predictable exception if the native cannot be loaded on the platform.
So once you can bundle the natives, they can be loaded as easily as this, with all the OS-specific naming conventions handled by JNILoader.
public class NativeSystemBLAS extends com.github .fommil .netlib .F2jBLAS {
static {
String jnilib = com.github .fommil .jni .JniNamer .getJniName ("netlib-native_system" );
String natives = System.getProperty ("com.github.fommil.netlib.NativeSystemBLAS.natives" , jnilib);
com.github .fommil .jni .JniLoader .load (natives.split ("," ));
}
...
}
And finally back onto the JVM, with a cleaned-up chart, the good news is that thanks to netlib-java
you get system optimised performance for free. And the overhead of "going native" is negligible. All the performance charts you've seen so far in Fortran and C are the same from Java --- and Scala.
Which leaves only one thing, which is a clean API.
Breeze
Source: https://github.com/scalanlp/breeze/wiki/Quickstart
Breeze uses netlib-java under the hood, and although it doesn't support all the netlib functions, it is using the most common ones and uses the same data structures so that you can drop down to the low-level netlib if needed.
This is the sbt dependency, this is the standard "import all the things" statement and this is how to create an empty dense vector.
Breeze is true to the netlib convention of storing transposes as bit flags, so a transpose on a vector doesn't actually do anything except wrap it in a Transpose
type.
libraryDependencies ++= Seq(
"org.scalanlp" %% "breeze" % "0.10" ,
"org.scalanlp" %% "breeze-natives" % "0.10"
)
Nov 30, 2014 6:42:51 PM com.github.fommil.jni.JniLoader liberalLoad
INFO: successfully loaded /tmp/jniloader7150057786941522144netlib-native_system-linux-x86_64.so
scala> import breeze.linalg ._
scala> val x = DenseVector.zeros [Double](5 )
x: DenseVector[Double] = DenseVector (0.0 , 0.0 , 0.0 , 0.0 , 0.0 )
scala> x.t
res1: Transpose[DenseVector[Double]] = Transpose (DenseVector (0.0 , 0.0 , 0.0 , 0.0 , 0.0 ))
Dense matrices are similar.
Columns can be accessed as DenseVectors, and rows as DenseMatrices and are mutable. I think I've successfully convinced you that the netlib specific mutable data structures are the only way to achieve high performance.
Note that Breeze has support for generic types on its matrices. Be careful, only Double
and Float
matrices are supported by natives.
scala> val m = DenseMatrix.zeros [Int](5 ,5 )
m: DenseMatrix[Int] =
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
scala> (m.rows , m.cols )
(Int, Int) = (5 ,5 )
scala> m (::,1 )
DenseVector[Int] = DenseVector (0 , 0 , 0 , 0 , 0 )
scala> m (4 ,::) := DenseVector (1 ,2 ,3 ,4 ,5 ).t
DenseMatrix[Int] = 1 2 3 4 5
scala> m
DenseMatrix[Int] =
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
1 2 3 4 5
There is a wide variety of operations and conveniences made available in Breeze. I recommend reading the cheatsheet. Here are just some of the things you can do with the nice API:
Elementwise addition
a + b
Elementwise multiplication
a :* b
Vector dot product
a dot b
Elementwise sum
sum(a)
Elementwise max
a.max
Elementwise argmax
argmax(a)
Linear solve
a b
Transpose
a.t
Determinant
det(a)
Inverse
inv(a)
Pseudoinverse
pinv(a)
SVD
SVD(u,s,v) = svd(a)
Example: Canon Ball
Lets take another look at that canon ball application of the Kalman Filter and see how to implement it in Breeze.
The caveat here is that this example is designed to show off the clean API of Breeze, but the matrices are so small that we don't actually get any benefit in using the natives. We'll look at a larger example shortly.
This is how we set up the problem
val dt = 0.1
val g = 9.8
def I = DenseMatrix.eye [Double](4 )
val B = DenseMatrix (
(0.0 , 0.0 , 0.0 , 0.0 ),
(0.0 , 0.0 , 0.0 , 0.0 ),
(0.0 , 0.0 , -1.0 , 0.0 ),
(0.0 , 0.0 , 0.0 , -1.0 )
)
val u = DenseVector (0 , 0 , g * Δt * Δt, g * Δt)
val F = DenseMatrix (
(1.0 , dt, 0.0 , 0.0 ),
(0.0 , 1.0 , 0.0 , 0.0 ),
(0.0 , 0.0 , 1.0 , dt),
(0.0 , 0.0 , 0.0 , 1.0 )
)
val H = I
val Q = DenseMatrix.zeros [Double](4 , 4 )
val R = I * 0.2
// guess of state and variance
var s = DenseVector.zeros [Double](4 )
var P = I
And this is the simulation with the Kalman inference.
Just to be clear, we've implemented a Kalman filter in about 7 lines of Scala code, once everything is all set up.
while (x (2 ) >= 0 ) {
// measurement
def noisy (actual: Double) = actual + Random.nextGaussian * 50
val z = x.mapValues (noisy)
// actual simulation
x = F * x + B * u
t += dt
// prediction step
val predS = F * s + B * u
val predP = F * P * F.t + Q
// observation step
val innov = z - H * predS
val innov_cov = H * predP * H.t + R
// update step
val gain = predP * H.t * inv (innov_cov)
s = predS + gain * innov
P = (I - gain * H) * predP
}
Source: http://greg.czerniak.info/guides/kalman1/
This is a simple application of the Kalman filter to the flight trajectory of a canon ball. In this example there is no input back into the system - we can't move the canonball (input), although gravity can - there is no noise in our process model and the observation model is perfect (it wouldn't be in 3D), but there is some noise in observation.
We only observe the green dots at every timestep, but we're still able to predict the next location in red, which is comparable to the true (blue) location.
Example: PCA
Remember back to the section on Principal Components Analysis, where we find the eigenvectors and eigenvalues of a matrix. If we take the eigenvectors with the highest eigenvalues, and throw away everything else we get an approximation of the original matrix. That is actually a pretty effective compression algorithm, because it's effectively the same thing as dimensional reduction.
So let's take an image as an example. In this code we convert a grayscale image into a matrix and then compute its SVD decomposition.
Then, we only keep the \(i\) top columns of \(u\) , top elements of \(s\) and top rows of \(v\) . Multiplying these out still gives a matrix the same size as the original but the memory required to store u, s and v is far less than storing the entire matrix.
val jpg = ImageIO.read (file ("input.jpg" ))
val orig = imageToMatrix (jpg)
val svd.SVD (u, s, v) = svd (orig)
for {
i <- 1 until u.cols
if i <= 100 // no discernable difference beyond!
} {
val compressed = u (::, 0 until i) * diag (s (0 until i)) * v (0 until i, ::)
val out = f"compressed-$i%03d.bmp"
val converted = matrixToImage (compressed)
ImageIO.write (converted, "BMP" , file (out))
}
And lets see what it looks like. In this video progression we start with the dimension, i, = 1 and then work our way up. i can go all the way to 500 but we give up after 100 because the changes become so insignificant after that.
Your browser does not support the video tag.