CUDA Libraries简介
CUDA Libraries簡介
?
上圖是CUDA 庫的位置,本文簡要介紹cuSPARSE、cuBLAS、cuFFT和cuRAND,之后會介紹OpenACC。
- cuSPARSE線性代數(shù)庫,主要針對稀疏矩陣之類的。
- cuBLAS是CUDA標準的線代庫,不過沒有專門針對稀疏矩陣的操作。
- cuFFT傅里葉變換
- cuRAND隨機數(shù)
CUDA庫和CPU編程所用到的庫沒有什么區(qū)別,都是一系列接口的集合,主要好處是,只需要編寫host代碼,調(diào)用相應API即可,可以節(jié)約很多開發(fā)時間。而且我們完全可以信任這些庫能夠達到很好的性能,寫這些庫的人都是在CUDA上的大能,一般人比不了。當然,完全依賴于這些庫而對CUDA性能優(yōu)化一無所知也是不行的,我們依然需要手動做一些改進來挖掘出更好的性能。
下圖是《CUDA C編程》中提到的一些支持的庫,具體細節(jié)可以在NVIDIA開發(fā)者論壇查看:
?
?
如果大家的APP屬于上面庫的應用范圍,非常建議大家使用。
A Common Library Workflow
下面是一個使用CUDA庫的具體步驟,當然,各個庫的使用可能不盡相同,但是不會逃脫下面的幾個步驟,差異基本上就是少了哪幾步而已。
下面是這幾個步驟的一些細節(jié)解釋:
Stage1:Creating a Library Handle
CUDA庫好多都有一個handle的概念,其包含了該庫的一些上下文信息,比如數(shù)據(jù)格式、device的使用等。對于使用handle的庫,我們第一步就是初始化這么一個東西。一般的,我們可以認為這是一個存放在host對程序員透明的object,這個object包含了跟這個庫相關聯(lián)的一些信息。例如,我們可定希望所有的庫的操作運行在一個特別的CUDA stream,盡管不同的庫使用不同函數(shù)名字,但是大多數(shù)都會規(guī)定所有的庫操作以一定的stream發(fā)生(比如cuSPARSE使用cusparseSetSStream、cuBLAS使用cublasSetStream、cuFFT使用cufftSetStream)。stream的信息就會保存在這個handle中。
Stage2:Allocating Device Memory
本文所講的庫,其device存儲空間的分配依然是cudaMalloc或者庫自己調(diào)用cudaMalloc。只有在使用多GPU編程的庫時,才會使用一些定制的API來實現(xiàn)內(nèi)存分配。
Stage3:Converting Inputs to a Library-Supported Format
如果APP的數(shù)據(jù)格式和庫要求的輸入格式不同的話,就需要做一次轉(zhuǎn)化。比如,我們APP存儲一個row-major的2D數(shù)組,但是庫卻要求一個column-major,這就需要做一次轉(zhuǎn)換了。為了最優(yōu)性能,我們應該盡量避免這種轉(zhuǎn)化,也就是盡量和庫的格式保持一致。
Stage4:Populating Device Memory with Inputs
完成上述三步后,就是將host的數(shù)據(jù)傳送到device了,也就是類似cudaMemcpy的作用,之所說類似,是引文大部分庫都有自己的API來實現(xiàn)這個功能,而不是直接調(diào)用cudaMemcpy。例如,當使用cuBLAS的時候,我們要將一個vector傳送到device,使用的就是cubalsSetVector,當然其內(nèi)部還是調(diào)用了cudaMemcpy或者其他等價函數(shù)來實現(xiàn)傳輸。
Stage5:Configuring the Library
有步驟3知道,數(shù)據(jù)格式是個明顯的問題,庫函數(shù)需要知道自己應該使用什么數(shù)據(jù)格式。某些情況下,類似數(shù)據(jù)維度之類的數(shù)據(jù)格式信息會直接當做函數(shù)參數(shù)配置,其它的情形下,就需要手動來配置下之前說的庫的handle了。還有個別情況是,我們需要管理一些分離的元數(shù)據(jù)對象。
Stage6:Executing
執(zhí)行就簡單多了,做好之前的步驟,配置好參數(shù),直接調(diào)用庫API。
Stage7:Retrieving Results from Device Memory
這一步將計算結果從device送回host,當然還是需要注意數(shù)據(jù)格式,這一步就是步驟4的反過程。
Stage8:Converting Back to Native Format
如果計算結果和APP的原始數(shù)據(jù)格式不同,就需要做一次轉(zhuǎn)化,這一步是步驟3的反過程。
Stage9:Releasing CUDA Resources
如果上面步驟使用的內(nèi)存資源不再使用就需要釋放掉,正如我們以前介紹的那樣,內(nèi)存的分配和釋放是非常大的負擔,所以希望盡可能的資源重用。比如device Memory、handles和CUDA stream這些資源。
Stage10:Continuing with the Application
繼續(xù)干別的。
再次重申,上面的步驟可能會給大家使用庫是非常麻煩低效的事兒,但其實這些步驟一般是冗余的,很多情況下,其中的很多步驟是不必要的,在下面的章節(jié)我們會介紹幾個主要的庫以及其簡要使用,相信看過后,你就不會認為使用庫得不償失了。
THE CUSPARSE LIBRARY
cuSPARSE就是一個線性代數(shù)庫,對稀疏矩陣之類的操作尤其獨到的用法,使用很寬泛。他當對稠密和稀疏的數(shù)據(jù)格式都支持。
下圖是該庫的一些函數(shù)調(diào)用,從中可以對其功能有一個大致的了解。cuSPARSE將函數(shù)以level區(qū)分,所有l(wèi)evel 1的function僅操作稠密和稀疏的vector。所有l(wèi)evel2函數(shù)操作稀疏矩陣和稠密vector。所有l(wèi)evel3函數(shù)操作稀疏和稠密矩陣。
?
cuSPARSE Data Storage Formats
稠密矩陣就是其中的值大部分非零。稠密矩陣所有值都是存儲在一個多維的數(shù)組中的。相對而言,稀疏矩陣和vector中元素主要是零,所以其存儲就可以做一些文章。比如我們可以僅僅保存非零值和其坐標。cuSPARSE支持很多種稀疏矩陣的存儲方式,本文只介紹其中三種。
先看一下稠密(dens)矩陣的存儲方式,圖示很明顯,不多說了:
?
Coordinate(COO)
對于稀疏矩陣中的每個非零值,COO方式都保存其行和列坐標,因此,當通過行列檢索矩陣值的時候,如果該行列值沒有在存儲格式中匹配到的話,必然就是零了。
我們應該注意到了,所謂稀疏矩陣要稀疏到什么程度才能使用COO呢?這個需要具體問題具體分析了,主要跟元素數(shù)據(jù)類型和索引數(shù)據(jù)類型有關。比如,一個存儲32-bit的浮點類型數(shù)據(jù)的稀疏矩陣,索引使用32-bit的整型格式,那么只有當非零數(shù)據(jù)少于于矩陣的三分之一的時候才會節(jié)約存儲空間。
?
Compressed Sparse Row(CSR)
CSR和COO相似,唯一不同就是非零值的行索引。COO模式下,所有非零值都會對應一個int的行索引,而CSR則是存儲一個偏移值,這個偏移值是所有屬于同一行的值擁有的屬性。如下圖所示,相比COO,減少了row:
?
因為所有存儲在同一行的數(shù)據(jù)在內(nèi)存中是相鄰的,要找到某一行對應的值只需要一個偏移量和length。例如,如果只想知道第三行的非零值,我們可以使用偏移量為2,length為2在V中檢索,如下圖所示:
?
對圖中的C使用相同的偏移和length就能定位列索引,也就能完全確定一個value在矩陣中的位置。當存儲一個很大的矩陣且相對來說每行數(shù)據(jù)都很多的時候,使用CSR比存儲每個非零值的索引要有效得多。
現(xiàn)在我們要考慮這些偏移地址和length的存儲了,最簡單的方式是創(chuàng)建兩個數(shù)組Ro和Rl,每個都對應一個nRows用作length。如果矩陣有大量的行就需要分配兩個很大的數(shù)組。鑒于此,我們可以使用單獨的一個length為nRows+1的數(shù)組R,第i行的偏移地址就存儲在R[i]。第i行的長度可以通過比較R[I+1]和R[i]值來做出判斷,還有就是R[i+1]是用來存儲矩陣非零值的總數(shù)的。本例中R數(shù)組如下:
?
由上圖知,0行的偏移地址是0,1行偏移地址是1,2行偏移地址是2,共有4個非零元素,我們可以找矩陣行為0的值及其列索引,由于R[1]-R[0]=1-0=1,說明第一行僅有一個非零值,其列索引為0,其值為3。
這樣,對于每行都有多個非零值的稀疏矩陣存儲,CSR比COO要節(jié)約空間。下圖是CSR的完整示意圖:
?
使用CSR格式稀疏矩陣的function很直觀,首先,我們在host定義一個CSR格式的稀疏矩陣,其代碼如下:
float *h_csrVals; int *h_csrCols; int *h_csrRows;h_csrVals用來存儲非零值個數(shù),h_csrCols存儲列索引,h_csrRows存儲行偏移,接下來就是分配device內(nèi)存之類的常規(guī)操作:
cudaMalloc((void **)&d_csrVals, n_vals * sizeof(float)); cudaMalloc((void **)&d_csrCols, n_vals * sizeof(int)); cudaMalloc((void **)&d_csrRows, (n_rows + 1) * sizeof(int)); cudaMemcpy(d_csrVals, h_csrVals, n_vals * sizeof(float),cudaMemcpyHostToDevice); cudaMemcpy(d_csrCols, h_csrCols, n_vals * sizeof(int),cudaMemcpyHostToDevice); cudaMemcpy(d_csrRows, h_csrRows, (n_rows + 1) * sizeof(int),cudaMemcpyHostToDevice);上述三種(包括稠密矩陣)數(shù)據(jù)格式各有各擅長的方面。下圖列出了cuSPARSE支持的一些數(shù)據(jù)格式以及各自的最佳使用場景:
?
Formatting Conversion with cuSPARSE
從前文可知,這個過程應該盡量避免,轉(zhuǎn)換不僅需要有計算的開銷,還有額外存儲的空間浪費。還有就是在使用cuSPARSE也應該盡量發(fā)揮其在稀疏矩陣存儲上的優(yōu)勢,因為好多APP的latency就是僅僅簡單的使用稠密矩陣存儲方式。因為cuSPARSE的數(shù)據(jù)格式眾多,其用來轉(zhuǎn)換的API也不少,下圖列出了這些轉(zhuǎn)換API。左邊那一列是你要轉(zhuǎn)換的目標格式,為空表示不支持兩種數(shù)據(jù)格式的轉(zhuǎn)換,盡管如此,你還可以通過多次轉(zhuǎn)換來實現(xiàn)未顯示支持的轉(zhuǎn)換API,比如dense2bsr沒有被支持,但是我們可以使用dense2csr和csr2bsr兩個過程來達到目的。
?
Demonstrating cuSPARSE
這部分示例代碼會涉及到矩陣向量相乘,數(shù)據(jù)格式轉(zhuǎn)換,以及其他cuSPARSE的特征。
// Create the cuSPARSE handle cusparseCreate(&handle); // Allocate device memory for vectors and the dense form of the matrix A ... // Construct a descriptor of the matrix A cusparseCreateMatDescr(&descr); cusparseSetMatType(descr, CUSPARSE_MATRIX_TYPE_GENERAL); cusparseSetMatIndexBase(descr, CUSPARSE_INDEX_BASE_ZERO); // Transfer the input vectors and dense matrix A to the device ... // Compute the number of non-zero elements in A cusparseSnnz(handle, CUSPARSE_DIRECTION_ROW, M, N, descr, dA,M, dNnzPerRow, &totalNnz); // Allocate device memory to store the sparse CSR representation of A ... // Convert A from a dense formatting to a CSR formatting, using the GPU cusparseSdense2csr(handle, M, N, descr, dA, M, dNnzPerRow,dCsrValA, dCsrRowPtrA, dCsrColIndA); // Perform matrix-vector multiplication with the CSR-formatted matrix A cusparseScsrmv(handle, CUSPARSE_OPERATION_NON_TRANSPOSE,M, N, totalNnz, &alpha, descr, dCsrValA, dCsrRowPtrA,dCsrColIndA, dX, &beta, dY); // Copy the result vector back to the host cudaMemcpy(Y, dY, sizeof(float) * M, cudaMemcpyDeviceToHost);上述代碼的過程可以總結為:
編譯:
$ nvcc -lcusparse cusparse.cu –o cusparse
Important Topics in cuSPARSE Development
盡管cuSPARSE提供了一個相對來說最快速和簡潔的方式以達到高性能的線性代數(shù)庫,我們?nèi)孕枰斢沜uSPARSE使用的一些關鍵點。
第一點就是,要保證正確的矩陣和向量的數(shù)據(jù)格式,cuSPARSE本身沒有什么能力來檢測出錯誤的或者不恰當?shù)臄?shù)據(jù)格式,而一次錯誤的格式操作就可能導致段錯誤,這也算是給自己debug提供一種方向吧,雖然段錯誤五花八門。對于矩陣和向量規(guī)模比較小的情況下,手動驗證其數(shù)據(jù)格式還是可行的。我們可以將轉(zhuǎn)換后的數(shù)據(jù)進行一次逆轉(zhuǎn)換過程來和原始數(shù)據(jù)比對。
第二點是cuSPARSE的默認異步行為。當然這對于GPU編程來說,已經(jīng)習以為常了,但是對于傳統(tǒng)的host端阻塞式的數(shù)學庫來說,GPU的計算結果會很有趣。對于cuSPARSE來說,如果使用了cudaMemcpy拷貝數(shù)據(jù)后,host會自動阻塞住,等待device的計算結果。但是如果cuSPARSE庫被配置來使用CUDA steam和cudaMemcpyAsync,我們就需要多留一個心眼,使用確保正確的同步行為來獲取device的計算結果。
最后一點比較新奇的是標量的使用,這里要使用標量的引用形式。如下代碼中的beta變量:
float beta = 4.0f; ... // Perform matrix-vector multiplication with the CSR-formatted matrix A cusparseScsrmv(handle, CUSPARSE_OPERATION_NON_TRANSPOSE, M, N, totalNnz, &alpha, descr, dCsrValA, dCsrRowPtrA, dCsrColIndA, dX, &beta, dY);如果不小心直接傳遞了beta這個參數(shù),APP會報錯(SEGFAULT),不注意的話這個bug很不好查。除此外,當標量作為輸出參數(shù)時,可以使用指針。cuSPARSE提供了cusparseSetPointMode這個API來調(diào)整是否使用指針來獲取計算結果。
THE cuBLAS LIBRARY
cuBLAS也是一個線代庫,不同于cuSPARSE,cuBLAS傳統(tǒng)線代庫的接口,BLAS即Basic Linear Algebra Subprograms的意思。cuBLAS level1是專門的vector之間操作。level2是矩陣和向量之間的操作。level3是矩陣和矩陣之間的操作。相對于cuSPARSE,cuBLAS不支持稀疏矩陣數(shù)據(jù)格式,他只支持而且善于稠密矩陣和向量的使用。
由于BLAS庫最初是由FORTRAN語言編寫,他就是用了column-major和one-based的方式存儲數(shù)據(jù),而cuSPARSE則是使用的row-major。下圖是這種方式的存儲格式,一看便明:
?
我們可以比較下row-major和column-major將二維轉(zhuǎn)化為一維的過程公式:
?
為了考慮兼容性,cuBLAS也使用了column-major的方式存儲,所以,對于習慣C/C++的人來說,這可能比較讓人困惑吧。
另一方面,就像C和其它語言那樣,one-based索引意味著數(shù)組中第一個元素的引用使用1而不是0,也就是說,一個有N個元素的數(shù)組,其最后一個值的索引是N而不是N-1。
但是,cuBLAS沒有辦法決定C/C++(cuBLAS使用C/C++)編程的語境,所以他就必須使用zero-based索引,這就導致了一個奇怪的混亂情況,要滿足FORTRAN的column-major,但one-based卻不行。
cuBLAS提出了兩個API,cuBLASASLegacy API是cuBLAS最開始的一個實現(xiàn),已經(jīng)廢棄,當前使用cuBLAS API,二者差異很小。
看過接下來的內(nèi)容,你會發(fā)現(xiàn),cuBLAS的使用流程跟cuSPARSE有很多相同之處,所以對于這些庫代碼編寫基本可以觸類旁通。
Managing cuBLAS Data
相較于cuSPARSE,cuBLAS的數(shù)據(jù)格式要簡單的多,所有操作都作用在稠密向量或矩陣。同樣是使用cudaMalloc來分配device內(nèi)存空間,但是使用cublasSetVector/cublasGetVector和cubalsSetMartix/cublasGetMartix在device和host之間傳送數(shù)據(jù)(其實相對cuSPARSE也沒多大差別)。本質(zhì)上,這些API底層都是調(diào)用cudaMemcpy,而且,他們對Strided和unstrided數(shù)據(jù)都有很好的優(yōu)化,比如下面的代碼:
cublasStatus_t cublasSetMatrix(int rows, int cols, int elementSize,const void *A, int lda, void *B, int ldb);
這些參數(shù)大部分看名字就知道什么意思了,其中l(wèi)da和ldb指明了源矩陣A和目的矩陣B的主維度(leading dimension),所謂主維就是矩陣的行總數(shù),這個參數(shù)只在需要host矩陣一部分數(shù)據(jù)的時候很有用。也就是說,當需要完整的矩陣時,lda和ldb都應該是M。
如果我們使用一個稠密的二維column-major的矩陣A,其元素是單精度浮點類型,矩陣大小為MxN,則使用下面的函數(shù)傳輸矩陣:
cublasSetMatrix(M, N, sizeof(float), A, M, dA, M);
也可以如下傳輸一個只有一列的矩陣A到一個向量dV:
cublasStatus_t cublasSetVector(int n, int elemSize, const void *x, int incx,void *y, int incy);
x是host上源起始地址,y是device上目的起始地址,n是要傳送數(shù)據(jù)的總數(shù),elemSize是每個元素的大小,單位是byte,incx/incy是要傳送的元素之間地址間隔,或者叫步調(diào),傳送一個單列長度M的column-major 矩陣A到向量dV如下:
cublasSetVector(M, sizeof(float), A, 1, dV, 1);
也可以如下傳送一個單行的矩陣A到一個向量dV:
cublasSetVector(N, sizeof(float), A, M, dV, 1);
通過這些例子可以發(fā)現(xiàn),使用cuBLAS要比cuSPARSE容易的多,所以除非我們的APP對稀疏矩陣需求比較大,一般都是用cuBLAS,保證性能的同時,還能提高開發(fā)效率。
Demonstrating cuBLAS
這部分代碼主要關注cuBLAS的一些統(tǒng)一使用并理解他為什么易于使用。得益于GPU的高性能計算,表現(xiàn)要比在CPU上的BLAS號15倍,而且cuBLAS的開發(fā)也就比傳統(tǒng)的BLAS稍微費事兒。
// Create the cuBLAS handle cublasCreate(&handle); // Allocate device memory cudaMalloc((void **)&dA, sizeof(float) * M * N); cudaMalloc((void **)&dX, sizeof(float) * N); cudaMalloc((void **)&dY, sizeof(float) * M); // Transfer inputs to the device cublasSetVector(N, sizeof(float), X, 1, dX, 1); cublasSetVector(M, sizeof(float), Y, 1, dY, 1); cublasSetMatrix(M, N, sizeof(float), A, M, dA, M); // Execute the matrix-vector multiplication cublasSgemv(handle, CUBLAS_OP_N, M, N, &alpha, dA, M, dX, 1,&beta, dY, 1); // Retrieve the output vector from the device cublasGetVector(M, sizeof(float), dY, 1, Y, 1);使用cuBLAS比較直觀,易于理解,其使用步驟基本如下:
編譯命令:
$ nvcc -lcublas cublas.cu
Porting from BLAS
將一個傳統(tǒng)的C實現(xiàn)的APP(使用BLAS庫)轉(zhuǎn)化為cuBLAS也是比較直觀的,基本可以歸納為以下幾步:
其等價的BLAS代碼是:
void cblas_sgemv(const CBLAS_ORDER order, const CBLAS_TRANSPOSE TransA,const MKL_INT M, const MKL_INT N, const float alpha, const float *A,const MKL_INT lda, const float *X, const MKL_INT incX, const float beta, float *Y,const MKL_INT incY);二者還是有很多相似之處的,不同的是,BLAS有個order參數(shù)來使用戶能夠指定輸入數(shù)據(jù)是row-major還是column-major。還有就是BLAS的beta和alpha沒有使用引用形式,
4. 最后就是在實現(xiàn)功能后調(diào)節(jié)性能了,比如:
- 復用device資源而不是釋放。
- device和host之間數(shù)據(jù)傳輸盡量減少冗余數(shù)據(jù)。
- 使用stream-based執(zhí)行來實現(xiàn)異步傳輸。
Important Topics in cuBLAS Development
相較于cuSPARSE,如果大家對BLAS熟悉的話,cuBLAS更容易上手。不過需要注意的是,雖然cuBLAS的行為更容易理解,但是有時候恰恰是這份理所當然的理解會造成一些認識誤區(qū),畢竟cuBLAS并不等于BLAS。
對于大部分習慣于row-major的編程語言,使用cuBLAS就得分外小心了,我們可能很熟悉將一個row-major的多維數(shù)組展開,但是過度到column-major會有點不適應,下面的宏定義可以幫我們實現(xiàn)row-major到column-major的轉(zhuǎn)換:
#define R2C(r, c, nrows) ((c) * (nrows) + (r))
不過,當使用上述的宏時,仍然需要一些循環(huán)的順序問題,對于C/C++程序猿來說,會經(jīng)常用下面的代碼:
for (int r = 0; r < nrows; r++) {for (int c = 0; c < ncols; c++) {A[R2C(r, c, nrows)] = ...} }代碼當然沒什么問題,但是卻不是最優(yōu)的,因為他在訪問A的時候,不是線性掃描內(nèi)存空間的。如果nrows非常大的話,cache命中率基本為零了。因此,我們需要下面這樣的代碼:
for (int c = 0; c < ncols; c++) {for (int r = 0; r < nrows; r++) {A[R2C(r, c, nrows)] = ...} }所以,做優(yōu)化要步步小心,因為一個沒注意,就有可能導致很差的cache命中。
cuFFT
未完待續(xù)~~~
?
?
參考書:《professional cuda c programming》
總結
以上是生活随笔為你收集整理的CUDA Libraries简介的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java声明公共构造函数_确保控制器具有
- 下一篇: 有mysql文件怎么运行不了_MySQL