上一篇:从零开始的 Rust 语言 blas 库之预备篇(1)—— blas 基础介绍
下一篇:TODO
文章部分参考:https://zhuanlan.zhihu.com/p/104287878
fortran 语言对于现在的程序员来说,显得有些奇怪,不仅在语法上,更加体现在实际的运用方面。
现在而言,通常情况下,我们表达一个矩阵,都是使用二维数组的,例如下面的矩阵 A \boldsymbol{A} A
A = [ 1 2 3 4 5 6 ] \boldsymbol{A}=\left [ \begin{matrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{matrix} \right ] A=⎣⎡135246⎦⎤
我们可以设置一个二维数组 a [ i ] [ j ] a[i][j] a[i][j],其中有
a [ 0 ] [ 0 ] = 1 , a [ 0 ] [ 1 ] = 2 a [ 1 ] [ 0 ] = 3 , a [ 1 ] [ 1 ] = 4 a [ 2 ] [ 0 ] = 5 , a [ 2 ] [ 1 ] = 6 a[0][0]=1,a[0][1]=2 \\ a[1][0]=3,a[1][1]=4 \\ a[2][0]=5,a[2][1]=6 a[0][0]=1,a[0][1]=2a[1][0]=3,a[1][1]=4a[2][0]=5,a[2][1]=6
这是一个非常朴素的想法,而且没有任何谬误。
不过二维数组的概念在 fortran77 那个年代或许有点超前,又或者不太实用,所以 fortran 中选择将二维数组压缩为一维数组。在这个压缩的过程中,势必会面临“列优先”或者“行优先”的问题。
简单地说,对于一个矩阵或者二维数组,有两种等价的表达方式。继续以上述的矩阵 A \boldsymbol{A} A 作为例子,使用一维数组 b b b 表示,则其行优先表示为
b [ 0 ] = 1 , b [ 1 ] = 2 , b [ 2 ] = 3 , b [ 3 ] = 4 , b [ 4 ] = 5 , b [ 5 ] = 6 m = 3 n = 2 b[0]=1,b[1]=2,b[2]=3,b[3]=4,b[4]=5,b[5]=6 \\ m=3 \\ n=2 b[0]=1,b[1]=2,b[2]=3,b[3]=4,b[4]=5,b[5]=6m=3n=2
其中矩阵可以表示为 m × n m\times n m×n 的形式。
事实上,在现在的 c 语言等高级编程语言中,对于上面的二维数组 a a a 而言,可以直接当作一维数组 b b b 使用,因为大多数语言中的多维数组,都是使用行优先的表示方式进行存储的。
虽然但是,fortran blas 偏偏不喜欢这一套,因为他是采用列优先的方式存储的。也就是说,如果使用一维数组 c c c 表示同一个矩阵 A \boldsymbol{A} A,结果为:
c [ 0 ] = 1 , c [ 1 ] = 3 , c [ 2 ] = 5 , c [ 3 ] = 2 , c [ 4 ] = 4 , c [ 5 ] = 6 m = 3 n = 2 c[0]=1,c[1]=3,c[2]=5,c[3]=2,c[4]=4,c[5]=6 \\ m=3 \\ n=2 c[0]=1,c[1]=3,c[2]=5,c[3]=2,c[4]=4,c[5]=6m=3n=2
矩阵仍然是 m × n m\times n m×n,但是存储的形式为列优先。
理解到这一层后,对于 blas 的 fortran 实现中的矩阵形态就没有迷惑了。
值得一提的是,我们可以发现,如果我们对矩阵转置得到 A T \boldsymbol{A}^T AT,则其原来的列优先存储形式的一维数组 c c c,就和转置后的行优先存储形式的一维数组 b T b^T bT 相同。(十分显然的结论)
这个结论的应用比较常见,例如对于 level 2 的 gemv 系列函数,其作用为计算
y ← α ∗ o p ( A ) ∗ x ⃗ + β ∗ y ⃗ y\gets \alpha*op(\boldsymbol{A})*\vec{x}+\beta*\vec{y} y←α∗op(A)∗x+β∗y
其中 A \boldsymbol{A} A 在传入函数时,我们一定认为其为 m × n m\times n m×n 的列优先矩阵。 o p ( A ) op(\boldsymbol{A}) op(A) 可为 A , A T \boldsymbol{A},\boldsymbol{A}^T A,AT 两者中的一种(可以通过参数控制),在这里我们认为 o p ( A ) = A op(\boldsymbol{A})=\boldsymbol{A} op(A)=A,即此时,我们正在计算
y ← α ∗ A ∗ x ⃗ + β ∗ y ⃗ y\gets \alpha*\boldsymbol{A}*\vec{x}+\beta*\vec{y} y←α∗A∗x+β∗y
虽然但是,如果好死不死,我们传入的矩阵 A \boldsymbol{A} A 为行优先矩阵,那么我们可以通过调控参数,使
o p ( A ) = A T op(\boldsymbol{A})=\boldsymbol{A}^T op(A)=AT
则我们也可以得到正确的结果。(动动手啊动动脑,想想这是为什么)
光是理解矩阵存储形式还是不足够理解 blas 函数中的功能,剩下一个关键点——lda
参数的含义仍然不明晰。下文仍然使用上面的列优先矩阵 A \boldsymbol{A} A 作为例子,大小为 m × n = 3 × 2 m\times n=3\times 2 m×n=3×2,对应的一维数组为 c c c。
计算的过程中,我们可能会使用矩阵的分块乘法的技巧来加速矩阵运算,因此我们需要能够在一个较大的矩阵 A \boldsymbol{A} A 中,表示其中的一个小部分 A ′ \boldsymbol{A}' A′。
此处,我们假设选取原来的矩阵的上部的 m ′ × n = 2 × 2 m'\times n=2\times 2 m′×n=2×2 部分,即
A ′ = [ 1 2 3 4 ] \boldsymbol{A}'=\left [ \begin{matrix} 1 & 2 \\ 3 & 4 \end{matrix} \right ] A′=[1324]
此时可以发现,矩阵 A ′ \boldsymbol{A}' A′ 的一维数组 c ′ c' c′ 的内容与原来的数组 c c c 不同,而且维度也不一样,所以我们自然想到需要创建一个新的数组,并对应的内容于 c ′ c' c′ 中。
这个想法非常朴素且可行,只可惜这样子内存消耗太大,如果矩阵大一点,分块次数多一些,迟早内存吃光光!因此,我们换一种思路——直接共用同一个一维数组指针 c c c。
但是直接使用 c c c 是不行的,这个时候就轮到 lda
出场力。lda,全称 leading dimension of array,就是用来解决子矩阵的问题。lda
的值大小,等于当前 X 优先的表示形式中,要使有效数据的第一个维度增加 1,所需要增加的大小。
回忆起原来的矩阵为
A = [ 1 2 3 4 5 6 ] \boldsymbol{A}=\left [ \begin{matrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{matrix} \right ] A=⎣⎡135246⎦⎤
注意到 c c c 采用列优先存储方式,如果将其直接当作二维数组,则其第一个维度即为列,第二个维度为行,因此 lda
的值为在 A ′ \boldsymbol{A}' A′ 中,要使列维度增加 1,在 c c c 中所需要增加的大小。
显然,在列优先形式下,此时 lda
的大小为原矩阵 A \boldsymbol{A} A 的行数;如果在行优先形式下,此时 lda
的大小则为原矩阵 A \boldsymbol{A} A 的列数。
直观的理解下,已知 lda
为 3,则我们对矩阵 A \boldsymbol{A} A 读取完左上的 1 和 3 之后,因为 A ′ \boldsymbol{A}' A′ 的大小为 2 × 2 2\times 2 2×2,所以我们需要读取下一列,因此我们根据 lda
的值,知道从这一列的开始元素 1 到下一列的开始元素之间,需要 3 个元素的地址偏移,之后就能得到下一列的开始元素为 2。
小朋友,明白了吗?
如果有好好理解上面的内容,对于 cblas 的封装,应该就会感觉很简单。
相对于 fortran 默认传入的矩阵的一维数组为列优先,在 cblas 中,允许用户指定传入的一维数组的存储形式。
事实上,cblas 只是使用上面的一个简单的结论一章中的内容,在 c 语言的层面,通过操控 o p ( A ) op(\boldsymbol{A}) op(A),从而改变存储形式,然后得到正确的结果罢了,因此这里没什么好说的。
例如对于 cblas_sgemv
函数,
/*
*
* cblas_sgemv.c
* This program is a C interface to sgemv.
* Written by Keita Teranishi
* 4/6/1998
*
*/
#include "cblas.h"
#include "cblas_f77.h"
void cblas_sgemv(const CBLAS_LAYOUT layout,
const CBLAS_TRANSPOSE TransA, const int M, const int N,
const float alpha, const float *A, const int lda,
const float *X, const int incX, const float beta,
float *Y, const int incY)
{
char TA;
#ifdef F77_CHAR
F77_CHAR F77_TA;
#else
#define F77_TA &TA
#endif
#ifdef F77_INT
F77_INT F77_M=M, F77_N=N, F77_lda=lda, F77_incX=incX, F77_incY=incY;
#else
#define F77_M M
#define F77_N N
#define F77_lda lda
#define F77_incX incX
#define F77_incY incY
#endif
extern int CBLAS_CallFromC;
extern int RowMajorStrg;
RowMajorStrg = 0;
CBLAS_CallFromC = 1;
if (layout == CblasColMajor)
{
if (TransA == CblasNoTrans) TA = 'N';
else if (TransA == CblasTrans) TA = 'T';
else if (TransA == CblasConjTrans) TA = 'C';
else
{
cblas_xerbla(2, "cblas_sgemv","Illegal TransA setting, %d\n", TransA);
CBLAS_CallFromC = 0;
RowMajorStrg = 0;
}
#ifdef F77_CHAR
F77_TA = C2F_CHAR(&TA);
#endif
F77_sgemv(F77_TA, &F77_M, &F77_N, &alpha, A, &F77_lda, X, &F77_incX,
&beta, Y, &F77_incY);
}
else if (layout == CblasRowMajor)
{
RowMajorStrg = 1;
if (TransA == CblasNoTrans) TA = 'T';
else if (TransA == CblasTrans) TA = 'N';
else if (TransA == CblasConjTrans) TA = 'N';
else
{
cblas_xerbla(2, "cblas_sgemv", "Illegal TransA setting, %d\n", TransA);
CBLAS_CallFromC = 0;
RowMajorStrg = 0;
return;
}
#ifdef F77_CHAR
F77_TA = C2F_CHAR(&TA);
#endif
F77_sgemv(F77_TA, &F77_N, &F77_M, &alpha, A, &F77_lda, X,
&F77_incX, &beta, Y, &F77_incY);
}
else cblas_xerbla(1, "cblas_sgemv", "Illegal layout setting, %d\n", layout);
CBLAS_CallFromC = 0;
RowMajorStrg = 0;
return;
}
可以简单理解一下,事实上 cblas 这一层允许指定外部数组的排布方式 layout
(行优先或列优先),并允许用户指定外部数组是否需要转置 TransA
,并根据这些信息,来最终选用在列优先的 fortran 中,是否需要一次矩阵转置。
为什么只需要零次或者一次矩阵转置呢?因为任意两次转置都会回到原来的矩阵,这也是一个简单的结论一节中的 trick。