CUDA學習——Chapter 3(7)歸約問題(1)相鄰歸約及並行歸約

第三章

歸約問題

首先來介紹一個並行計算的案例:
有一個長度爲n的數組L,求他們的和。
如果按順序疊加,那麼時間複雜度就是O(n)。
僞代碼就是:

count←0
for i←1 to n
	count+=L[i]
return count

那還有一種在串行是O(n),在並行上可以是log2n\left \lceil log_2n \right \rceil的算法,稱爲並行歸約算法。這種算法是基於二分法的思想來做的,其基本思路就是,把輸入的數組劃分成更小的基本單位,直到計算每一個基本單位的和都只需要一個加法指令,然後自底向上的合併這些結果,稱爲歸約。而並行計算的優勢在於並行,因此最底層的基本單位的和的計算就可以使用並行來處理,同時,得到的基本單位的和組成的集合可以就地保存在輸入向量中,不需要有額外的空間開銷。

那麼這些基本單位的劃分可以分爲兩種:相鄰配對和交錯配對。

還是拿上面的L來說,相鄰配對指的是相鄰的兩個元素組成一個基本單位,比如L[0]和L[1],L[2]和L[3],如果數組有奇數個元素,那麼最後就剩一個單獨的L[n-1]做基本單位。

交錯配對指的是,以n/2爲分界線,左右兩邊各自重新分序號,序號相同的組在一起。比如L[0]和L[4],L[1]和L[5](如果L的長度爲8),直到L[3]和L[7]。

下列的示意圖應該可以更好地幫助大家理解(3-19爲相鄰配對,3-20爲交錯配對):
相鄰配對
交錯配對

如何實現並行歸約?

首先,思路就是,每次都是線程全部並行計算完之後,再開始下一步的線程並行計算,直至活躍的線程只有一個。這個思路既可以是循環的,也可以是遞歸的。在Chapter 3(5)裏面我曾經介紹過一個設備端的API:__syncthreads,可以使用這個API來使得線程塊內的活躍線程全部到達同一個點。

特別需要注意的一點就是:線程塊之間是無法等待的,所以需要把每個線程塊得到的結果通過cudaMemcpy拷貝回主機端進行串行計算。

其設備端代碼如下:

__global void reduceNeighbored(int *g_idata,int *g_odata,unsigned int n)
{
	//set thread ID
	unsigned int tid=threadIdx.x;
	unsigned int idx=blockIdx.x*blockDim.x+threadIdx.x;
	int *idata=g_idata+blockIdx.x*blockDim.x;
	//boundary check
	if (idx>=n) return;
	//in-place reduction in global memory
	for(int stride=1;stride<blockDim.x;stride *=2){
	if((tid%(2*stride)==0){
		idata[tid]+=idata[tid+stride];
	}
	// synchronize within block
	__syncthreads();
	}
	// write result for this block to global mem
	if (tid==0) g_odata[blockIdx.x]=idata[0];

我們來分析一下這些代碼:

unsigned int tid=threadIdx.x;
unsigned int idx=blockIdx.x*blockDim.x+threadIdx.x;

第一條代碼得到線程在當前行中的唯一ID,第二條代碼得到該線程在當前塊中的唯一ID。

int *idata=g_idata+blockIdx.x*blockDim.x;

因爲g_idata是指向整個矩陣的指針,而blockIdx.x*blockDim.x可以得到當前塊與矩陣對應的行,從而得到偏移量。(blockIdx.x*blockDim.x也就是走過了多少個線程,記住,一個線程對應矩陣裏的一個元素)。

for(int stride=1;stride<blockDim.x;stride *=2)

blockDim.x是x方向上塊的最大線程數(也就是x方向上一行最多能有多少個線程)。傳入的idata是一行的輸入向量。

if((tid%(2*stride)==0){
		idata[tid]+=idata[tid+stride];
	}

stride一開始是1,也就是說會由tid爲偶數的線程(0、2、4、6)來完成相加操作。然後stride*2,就會由tid爲4的倍數的線程(0、4)完成下一步操作。
因爲每一次的for循環都意味着本次基本單位求和已結束。所以在開始下一次for循環之前要使用__syncthreads()函數來對線程進行同步。

	// write result for this block to global mem
	if (tid==0) g_odata[blockIdx.x]=idata[0];

因爲最後執行該行求和任務的是ID爲0的thread,所以只讓ID爲0的thread執行該操作,往g_odata的blockIdx.x的位置寫入結果。
爲什麼是blockIdx.x而不是其它的數?因爲blockIdx.x對應矩陣內的行數。
我們再來看看主機端的代碼(部分):

int main(int argc, char **argv)
{
	// initialization
    int size = 1 << 24; // total number of elements to reduce
    // execution configuration
    int blocksize = 512;   // initial block size
    dim3 block (blocksize, 1);
    dim3 grid  ((size + block.x - 1) / block.x, 1);
    // allocate host memory
    size_t bytes = size * sizeof(int);
    int *h_idata = (int *) malloc(bytes);
    int *h_odata = (int *) malloc(grid.x * sizeof(int));
    // initialize the array
    for (int i = 0; i < size; i++)
    {
        // mask off high 2 bytes to force max number to 255
        h_idata[i] = (int)( rand() & 0xFF );
    }
    int gpu_sum = 0;
    // allocate device memory
    int *d_idata = NULL;
    int *d_odata = NULL;
    CHECK(cudaMalloc((void **) &d_idata, bytes));
    CHECK(cudaMalloc((void **) &d_odata, grid.x * sizeof(int)));
    // kernel 1: reduceNeighbored
    CHECK(cudaMemcpy(d_idata, h_idata, bytes, cudaMemcpyHostToDevice));
    CHECK(cudaDeviceSynchronize());
    reduceNeighbored<<<grid, block>>>(d_idata, d_odata, size);
    CHECK(cudaDeviceSynchronize());
    CHECK(cudaMemcpy(h_odata, d_odata, grid.x * sizeof(int),
                     cudaMemcpyDeviceToHost));
    int gpu_sum = 0;
    for (int i = 0; i < grid.x; i++) gpu_sum += h_odata[i];
    // 驗證代碼略……
    return 0;
}

我們還是先比較細緻地分析一下部分的代碼:

	// initialization
    int size = 1 << 24; // total number of elements to reduce
    // execution configuration
    int blocksize = 512;   // initial block size
    dim3 block (blocksize, 1);
    dim3 grid  ((size + block.x - 1) / block.x, 1);

這段代碼聲明瞭一個長度爲512的一維塊向量。而整個矩陣的大小有1<<24,所以聲明瞭一個1<<24/512+1長度的塊(思考一下,矩陣的大小是1<<24,而長度爲512,那麼其寬度不就是1<<24/512,爲了防止小數不能被包含,向上取整,所以+1)。

	CHECK(cudaMemcpy(h_odata, d_odata, grid.x * sizeof(int),
                     cudaMemcpyDeviceToHost));
    int gpu_sum = 0;
    for (int i = 0; i < grid.x; i++) gpu_sum += h_odata[i];

h_odata裏面儲存着每行求和完後的代碼,因爲線程塊之間不可同步,所以每行的和的求和需要在主機端上進行。而線程塊的個數對應的變量就是grid.x,所以循環的長度爲grid.x。

驗證代碼其實就是在主機端使用O(n2)O(n^2)的矩陣求和代碼算一遍就可以了,然後把這個結果和GPU計算的結果相比對。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章