1. 程式人生 > >CUDA 共享記憶體 bank conflict

CUDA 共享記憶體 bank conflict

1. bank conflict

本文所有的實驗針對 GTX980 顯示卡,Maxwell 架構,計算能力 5.2。

GPU 共享記憶體是基於儲存體切換的架構(bank-switched-architecture)。在 Femi,Kepler,Maxwell 架構的裝置上有 32 個儲存體(也就是常說的共享記憶體分成 32 個bank),而在 G200 與 G80 的硬體上只有 16 個儲存體。

每個儲存體(bank)每個週期只能指向一次操作(一個 32bit 的整數或者一個單精度的浮點型資料),一次讀或者一次寫,也就是說每個儲存體(bank)的頻寬為 每週期 32bit。

如下圖所示,在一個執行緒塊中申請如下的共享記憶體:

__shared__ float sData[32][32];
  • 1

這裡寫圖片描述

也就是說在上述的 32 * 32 的二維陣列共享記憶體中,每一列對應同一個 bank。

  1. 同常量記憶體一樣,當一個 warp 中的所有執行緒訪問同一地址的共享記憶體時,會觸發一個廣播(broadcast)機制到 warp 中所有執行緒,這是最高效的。
  2. 如果同一個 warp 中的執行緒訪問同一個 bank 中的不同地址時將發生 bank conflict。
  3. 每個 bank 除了能廣播(broadcast)還可以多播(mutilcast)(計算能力 >= 2.0),也就是說,如果一個 warp 中的多個執行緒訪問同一個 bank 的同一個地址時(其他執行緒也沒有訪問同一個bank 的不同地址)不會發生 bank conflict。
  4. 即使同一個 warp 中的執行緒 隨機的訪問不同的 bank,只要沒有訪問同一個 bank 的不同地址就不會發生 bank conflict。

這裡寫圖片描述

如上圖所示,左側和右側的都沒有發生 bank conflict。而中間的存在 bank conflcit,由於經過最多兩次,該 warp 中的執行緒就都可以得到所要的資料,所有稱為 2-way bank conflict,如果同一個 warp 中的所有執行緒訪問一個 bank 中的 32 個不同地址,則需要分 32 次,稱為 32-way bank conflict。

這裡寫圖片描述

如上圖所示,左中右均未發生 bank conflict。

依次我們可以總結:只要同一個 warp 的不同執行緒會訪問到同一個 bank 的不同地址就會發生 bank conflict,除此之外的都不會發生 bank conflict。

既然廣播是針對同一個 warp 而言的,那麼如果不同的 warp 訪問同一個 bank 中的同一個地址呢?由於 每個 SM 中有 4 個 warp scheduler (GTX980),可以很好的排程 warp,使其 warp 之間的訪問衝突可以充分的隱藏,因此對效率的影響很小,遠遠小於 warp 內的 bank conflict。至於 warp scheduler 的排程機制,NVIDIA 沒有說的特別清楚,可能也是想要開發者不要過於關注於此。

2. 實驗 1

實現定義如下圖所示的 32 * 32 執行緒塊,共 1024 個執行緒,32 個 warp。

這裡寫圖片描述

申請如 1 中所示的 32 * 32 的共享記憶體,共 32 個 bank,每個 bank 對應 32 個元素。

  • 實驗 1.1
    該執行緒塊中的每個 warp 讀寫不同的 bank,不同的 warp 不會訪問一個地址,也就是一一對應的關係。圖中的數字就表示上圖中的執行緒標號。經分析可知,此時是沒有 bank conflict 的。

這裡寫圖片描述

程式碼如下:

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE];

if (x_id < col && y_id < row)
{
    sData[threadIdx.y][threadIdx.x] = matrix[index];
    __syncthreads();
    matrixTest[index] = sData[threadIdx.y][threadIdx.x];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 實驗 1.2
    該執行緒塊中的每個 warp 讀寫相同的 bank 的不同地址,不同的 warp 訪問不同,也就是一一對應的關係。圖中的數字就表示上圖中的執行緒標號。經分析可知,此時是存在很嚴重的 bank conflict 。

這裡寫圖片描述

程式碼如下:

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE];

if (x_id < col && y_id < row)
{
    sData[threadIdx.x][threadIdx.y] = matrix[index];
    __syncthreads();
    matrixTest[index] = sData[threadIdx.x][threadIdx.y];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 實驗 1.3(避免 bank conflict 的技巧)
    針對實驗 1.2 中出現的嚴重的 bank conflict,我們可以通過新增一個附加列來避免 bank conflict,如下圖所示,左圖為申請的共享記憶體矩陣形式,右圖是表示成 bank 後的形式,通過這種方式,原來在一個 bank 中的同一個 warp 都正好偏移到了不同的 bank 中。

這裡寫圖片描述

程式碼如下:

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE+1];

if (x_id < col && y_id < row)
{
    sData[threadIdx.x][threadIdx.y] = matrix[index];
    __syncthreads();
    matrixTest[index] = sData[threadIdx.x][threadIdx.y];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上述三個小實驗的執行時間為:

實驗 1.1 :0.052416 ms
實驗 1.2 :0.131072 ms
實驗 1.3 :0.053280 ms
  • 1
  • 2
  • 3

除去公共程式碼後的時間為:

實驗 1.1 :0.034816 ms
實驗 1.2 :0.113472 ms
實驗 1.3 :0.035680 ms
  • 1
  • 2
  • 3

結論:

  1. 通過額外的一行,可以避免 bank conflict,執行時間與完全沒有 bank conflict 的執行時間差距很小。
  2. 存在 bank conflict 的,執行時間幾乎是沒有 bank conflict 的執行時間的 4 倍。

其實只要新增的是奇數列就可以,只不過 1 列是最節省空間(共享記憶體太寶貴)的。

3. 實驗 2

  • 實驗 2.1
    同一個 block 中所有第 i 列的執行緒都計算第 i 行的元素的和,此時所有同一個warp 會訪問同一個 bank 的不同地址。如下圖所示,分別表示第 0 列訪問 bank 0 中的第一個地址,第 1 列訪問 bank 1 中的第 1 個地址,依次類推。

這裡寫圖片描述

程式碼如下:

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE];

if (x_id < col && y_id < row)
{
    sData[threadIdx.y][threadIdx.x] = matrix[index];
    __syncthreads();

    float data = 0.0f;
    for (int j = 0; j < BLOCKSIZE; j++)
    {
        data += sData[threadIdx.x][j];
    }
    matrixTest[index] = data;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 實驗 2.2
    同實驗 1.3 類似,新增額外的一列,如下圖所示:

這裡寫圖片描述

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE+1];

if (x_id < col && y_id < row)
{
    sData[threadIdx.y][threadIdx.x] = matrix[index];
    __syncthreads();

    float data = 0.0f;

    for (int j = 0; j < BLOCKSIZE; j++)
    {
        data += sData[threadIdx.x][j];
    }
    matrixTest[index] = data;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上述兩個實驗的執行時間如下所示:

實驗 2.1 :0.458144 ms
實驗 2.2 :0.090848 ms
  • 1
  • 2

從上圖也可以看出,修改後的頻寬相當於修改前的 32 倍。修改後的執行時間也明顯得到改善。

4. 實驗 3

  • 實驗 3.1
    採用實驗 1.1 的方式,同一個 warp 訪問不同的 bank,不同的 warp 訪問不同的地址。

程式碼如下:

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE];

if (x_id < col && y_id < row)
{
    sData[threadIdx.y][threadIdx.x] = matrix[index];
    __syncthreads();

    float data = 0.0f;
    for (int j = 0; j < 1000; j++)
    {
        data = sData[threadIdx.y][threadIdx.x];
    }
    matrixTest[index] = data;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 實驗 3.2
    同一個 warp 訪問不同的 bank,所有 warp 訪問同一個地址,也就是說所有的行都會訪問第 0 行。

程式碼如下:

int x_id = blockDim.x * blockIdx.x + threadIdx.x; // 列座標
int y_id = blockDim.y * blockIdx.y + threadIdx.y; // 行座標
int index = y_id * col + x_id;

__shared__ float sData[BLOCKSIZE][BLOCKSIZE];

if (x_id < col && y_id < row)
{
    sData[threadIdx.y][threadIdx.x] = matrix[index];
    __syncthreads();

    float data = 0.0f;
    for (int j = 0; j < 1000; j++)
    {
        data = sData[0][threadIdx.x];
    }
    matrixTest[index] = data;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上述兩個實驗的執行時間如下所示:

實驗 2.1 :0.053800 ms
實驗 2.2 :0.055328 ms
  • 1
  • 2

在實驗 2.2 中存在明顯的不同 warp 間的衝突,但是執行時間差距很小,也就是說 warp 間衝突的影響很小。

5. visual profiler

通過 visual profiler 可以判斷程式中是否存在 bank conflict,在執行 visual profiler 前需要新增 -lineinfo選項,在 visual studio 中可以設定,如下所示:

這裡寫圖片描述

在 visual profiler 中分析實驗 1.2,結果如下所示,可以直接定位到出現 bank conflict 的行。

這裡寫圖片描述

6. 完整程式碼

我的GitHub

7. 參考

  1. 《CUDA_C_Programming_Guide》7.0 Appendix G. COMPUTE CAPABILITIES / 4.1 / 4.2
  2. 《CUDA_C_Best_Practices_Guide》7.0
  3. 《CUDA 並行程式設計:GPU程式設計指南》6.4
  4. 《GPU 高效能運算之 CUDA》4.7.1.3/4.4.3
  5. 《Performance modeling of atomic additions on
    GPU scratchpad memory》
  6. stackoverflow

--------------------- 作者:木子超同學 來源:CSDN 原文:https://blog.csdn.net/endlch/article/details/47043069?utm_source=copy 版權宣告:本文為博主原創文章,轉載請附上博文連結!