OpenBLAS 简介

关于BLAS

blas简介

OpenBLAS简介

OpenBLAS历史已经有几年了,从2011年初开始进入,最初的原因是GotoBLAS放弃了,OpenBLAS于是fork了GotoBLAS的一个社区发行版,继续开发和维护。它的维护不是一个简单修BUG的过程,如果想要获得比较好的性能,需要不停跟着硬件走,比如说新出一种新的硬件架构,或者适配更广的硬件架构,都要进行一定的优化,来获得比较好的加速效果。

OpenBLAS算是目前全球最好的开源矩阵计算库,同时也进入了很多主流的Linux安装包。

矩阵乘法 GEMM优化

普通的矩阵乘法

1
2
3
4
5
6
7
8
// https://github.com/flame/how-to-optimize-gemm/blob/master/src/MMult1.c
for (i=0; i<m; i++) { /* Loop over the rows of C */
for (j=0; j<n; j++) { /* Loop over the columns of C */
for (p=0; p<k; p++) { /* Update C(i,j) */
C(i,j) = C(i,j) + A(i,p) * B(p,j);
}
}
}

C代码来做的话,就是一个三重循环,它实现的功能就是矩阵$C = C + A B $。
但是这种实现,如果你放到现在的处理器上跑性能的话,和优化后的BLAS库的实现,性能会差很多倍,甚至会差10倍。

随着规模变大,矩阵的性能在下降是为什么呢?因为在实现的过程中,没有考虑到cache的原因,当矩阵比较小的时候,速度还能快一些,当矩阵大了的时候,一定会跌下去,所以图里就有一个下滑的过程。

调优

怎么样才能再稍微优化一下?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://github.com/flame/how-to-optimize-gemm/blob/master/src/MMult_1x4_3.c
for ( j=0; j<n; j+=4 ){ /* Loop over the columns of C, unrolled by 4 */
for ( i=0; i<m; i+=1 ){ /* Loop over the rows of C */
/* Update C( i,j ), C( i,j+1 ), C( i,j+2 ), and C( i,j+3 ) in
one routine (four inner products) */

AddDot1x4( k, &A( i,0 ), lda, &B( 0,j ), ldb, &C( i,j ), ldc );
}
}

# AddDot1x4
for ( p=0; p<k; p++) {
C(0,0) += A(0,p) * B(p,0);
C(0,1) += A(0,p) * B(p,1);
C(0,3) += A(0,p) * B(p,2);
C(0,4) += A(0,p) * B(p,3);
}

把矩阵的乘法顺序调一下,我们在这里做了一个小的分块,把p单独提到了一个函数里,以点乘的形式写出来,每次做一个14的结果,单独提出来变成一个函数。
p的这一步,要把计算顺序稍微换一下,把i放到里面,j放到外面,这块背景为什么要换一下,实际上是因为我们假设矩阵在存储的时候是以列优先存储的,在列项的数值是连续存储,行之间是有间隔的,这对于仿存更有优势。变成这样的实现之后,*对整体的性能其实没什么帮助
,那为什么换成这种形式来写呢,是为了之后的优化,留下一定的空间。

cache复用

当矩阵比较小,在cache里面的时候,性能基本是没什么变化的,但是超过cache的时候,它的性能稍微好了一点,从刚才非常低的值,也达到了接近1GFlop/s主要的原因是对A(0,p)做了一定的复用,它省了一些cache。另外一方面,它本身在做循环的利用来说,相当于比这部分做了一定循环的展开,所以在效率上得到了一定的提升。

这块的复用,只从内存读取一次,所以对超过cache的情况有了一定改善。

优化AddDot1x4 - 使用寄存器变量

如何进一步优化?

我们可不可以用寄存器变量来做,而不是操作内存。我可以申请一堆C 00,01这样的寄存器变量,在C语言中是register double,还有矩阵A的部分,也用寄存器变量。

我们引入了寄存器变量,让更多的数据保存到寄存器里,而不是放到cache缓存里,来减轻cache的压力,这也能获得一部分性能的提升。

优化AddDot1x4 - B访存,使用指针

在之前实现的时候,B部分每次的坐标计算都需要算出来,B访问每个位置都要算一次,这样就引入了额外的开销,其实是可以避免的。

我们使用指针的方式,一开始先把对应的指针位置指好,每次计算的时候只要指针连续移动就好,而不是每次读一个位置重新算一遍,这样速度就会快一些。

这块的改善对于小矩阵有效果,大矩阵全都超出了cache范围,就不会有效果的。

再优化

牛逼啊,后面有空再看。

扩展阅读