2015年5月16日 星期六

BLAS 矩陣筆記

BLAS 是相當古早且好用的東西,全名是Basic Linear Algebra Subprograms,一如其名,它是作為線性代數而生的東西。
BLAS分別有 Level 1 ~Level 3 共三個級別,分別對應到三個不同複雜度的計算類型:向量-向量運算、矩陣-向量運算以及矩陣-矩陣運算,大概是加減乘的功能,它規範了一個標準的介面(API)給開發者,雖然未必完全遵守,但至少讓大家可以很方便的使用這類的功能,轉換各種函式庫的時候相當的方便。
剛學習 BLAS 的時候,我總是有個困惑:線性代數的功能不是很容易就能寫出了嗎?為什麼我不能自己寫就好了?還得去另外學別的API?
是的,簡單的矩陣相乘、相加這類的演算法大家都能實做出來,但在學習CUDA後,我大概瞭解這件事並不是那麼簡單,如果要讓實做出來的程式具有良好的效能,那得考量到許多硬體的特性,如記憶體、快取等等問題,另外矩陣相乘也有不同的演算法可以讓時間複雜度大大降低,總之,現存的BLAS並不是那麼簡單的東西,許多高階的軟體如matlab之類的底層都使用BLAS。
在BLAS中,最早被實現的是LAPACK這個庫,它是基於Fortran的一個庫,而它的API似乎也經常被拿來參考,而今天要記下的則是BLAS中經常讓我感到困惑的一個小概念,就是矩陣中的「lda」參數。
先來看一下 DGEMM 這個矩陣相乘的函數吧
DGEMM - perform one of the matrix-matrix operations   C := alpha*op( A )*op( B ) + beta*C,
DGEMM ( TRANSA, TRANSB, M, N, K, ALPHA, A, LDA,
                       B, LDB, BETA, C, LDC )
以上是這函數的簡介,那數學式相當簡單明瞭,但下面那函式的參數就讓人很困惑了,「LDA」「LDB」「LDC」這三個參數在數學式中完全沒有出現過,於是我去查了說明:
      LDA    - INTEGER.
On entry, LDA specifies the first dimension of A as
declared in the calling (sub) program. When  TRANSA =
'N' or 'n' then LDA must be at least  max( 1, m ),
otherwise  LDA must be at least  max( 1, k ).Unchanged on exit.
大致上說明了這是拿來指定DGEMM時A矩陣的第一個維度的長度,也就是假設你的矩陣是A[M][N],則這個地方輸入M即可。Well……如果是平常使用,那其實這樣也就夠了,反正M和N輸入一個隨便試總是會有正確的。但為什麼明明我已經輸入了M N了,他還要再跟我要一次矩陣的大小?這其實是有其意義的。
先回顧一下非BLAS的庫吧,以Eigen舉例,如果宣告一個 5 x 5 的矩陣,但我只需要取最左上角的 3 x 3 的矩陣進行運算要怎麼做?
官方文件的說明,做法如以下
M.block().block(0,0,3,3);
在BLAS中並沒有內建 .block 這類的函式,但它仍能靠「LDA」這類的參數實現這個功能。
在牽扯到矩陣的運算中,總會有幾個參數幾乎是必然會出現的,那就是 M、N、LDA(LDB和LDA性質相同,故不提),光是這三個參數就足夠實現 .block 的運算了。
假設我們有個 4 x 4 的矩陣如下

1 2 3 0
4 5 6 0
7 8 9 0
0 0 0 0
你可以說這是4x4,但多數情況下它不會只是4x4,而可能是100x100,但只有左上方的3x3為非零,我們會希望只取3 x 3 的矩陣進行運算,以節少許多運算時間。
這時我該怎麼做?在BLAS中的方法是:

原點=0
M=3
N=3
lda=4
在這裡,M和LDA有了不同的數值,相當的奇怪,但當我們仔細探討LDA這參數在這裡面的意義時就有了些不同。
在多數時候,一個3 x 3 的陣列儲存於電腦中時是使用1*9*sizeof(data_type)的記憶體儲存,而如果我們指定[2][2],通常是要取得第3*2+2個記憶體位址的值,以c++舉例,A[2][2]=*(A+3*2+2)。
但這和LDA有什麼關係呢? 關係可大了,LDA其實並不是單單標記矩陣的大小而已,它代表的是指標的位移量,如果把上面指定M N LDA的過程用中文詳述的話,大概如下:

『指標從原點開始向前走,走M格後,指標重新指向原點+LDA的位址,並繼續往前走M格,再把指標重新指向原點+2*LDA的位址,並繼續往前走M格』
也就是說,透過指定原點以及M N LDA 就可以達成.block的功能了。
另外,如果結果有出錯,那可能得檢查一下指標的宣告和變數的宣告是否有所不同,如果有不同的話那可能會造成指標指向錯誤的位置,而這種錯誤編譯器通常不會報錯,得小心注意。