簡體   English   中英

如何在 cuda 中獲得並行 arrays 的“總和”?

[英]How to get "sum" of parallel arrays in cuda?

我的問題是關於獲得相同長度 arrays 的“總和”。 例如,我總共有一個 M*N(100 * 2000) 長度的浮點數組。 我想獲得每個 N(2000) 浮點數的 M(100) 和值。 我找到了兩種方法來完成這項工作。 一種是在 M 的 for 循環中使用 Cublas function,例如cublasSasum 另一種是自寫的kernel function,循環加數字。 我的問題是這兩種方式的速度以及如何在它們之間進行選擇。

對於Cublas方法,無論N(4000~2E6)有多大,耗時主要取決於循環數M。

對於自寫的狗窩function,速度隨N變化很大。具體來說,如果N很小,低於5000,它比Cublas方式運行得快得多。 然后時間消耗隨着N的增加而增加。

N = 4000 |10000 | 40000 | 80000 | 1E6 | 2E6

t = 254ms| 422ms | 1365毫秒| 4361ms| 5399 毫秒 | 10635毫秒

如果 N 足夠大,它的運行速度會比 Cublas 方式慢得多。 我的問題是我怎么能用 M 或 N 來決定我應該使用哪種方式? 我的代碼可能用於不同的 GPU 設備。 我必須比較掃描參數中的速度,然后“猜測”以在每個 GPU 設備中做出選擇,還是可以從 GPU 設備信息中推斷?

此外,對於 kernel function 方式,我在決定blockSizegridSize時也有問題。 我必須在這里指出,我更關心的是速度而不是效率。 因為 memory 是有限的。 例如,如果我得到 8G memory。 我的數據格式是 4 個字節的浮點數。 N是1E5。 那么 M 最多為 2E4,小於MaxGridSize 所以我有兩種方法如下。 我發現有一個更大的 gridSize 總是更好,我不知道原因。 是關於每個線程的寄存器號的使用嗎? 但我認為在這個 kernel function 中每個線程不需要很多寄存器。

任何建議或信息將不勝感激。 謝謝你。

庫布拉斯方式

for (int j = 0;j<M;j++)
    cublasStatus = cublasSasum(cublasHandle,N,d_in+N*j,1,d_out+j);

自寫kernel方式

__global__ void getSum(int M, int N, float* in, float * out)
{
    int i = threadIdx.x + blockIdx.x * blockDim.x;
    if(i<M){
        float tmp = 0;
        for(int ii = 0; ii<N; ii++){
            tmp += *(in+N*i+ii);
        }
        out[i] = tmp;
    }
}

更大的 gridSize 更快。 我不知道原因。

getSum<<<M,1>>>(M, N, d_in, d_out); //faster
getSum<<<1,M>>>(M, N, d_in, d_out); 

這是一個blockSize-time參數掃描的結果。 M = 1E4.N = 1E5。

cudaEventRecord(start, 0);
//blockSize = 1:1024;
int gridSize = (M + blockSize - 1) / blockSize;
getSum<<<gridSize1,blockSize1>>>...
cudaEventRecord(stop, 0);
cudaEventSynchronize(stop);
cudaEventElapsedTime(&time, start, stop);

看來我應該選擇一個相對較小的blockSize ,比如 10~200。 我只是想知道為什么完全占用(blockSize 1024)比較慢。 我只是出於一些可能的原因在這里發帖,注冊號碼?延遲? 在此處輸入圖像描述

使用 CuBLAS 通常是一個非常好的主意,如果有專用的 function 可以直接滿足您的需求,則應該首選,尤其是對於大型數據集。 話雖如此,對於在如此小的數據集上工作的 GPU kernel 來說,您的時間安排非常糟糕。 讓我們理解為什么。

更大的 gridSize 更快。 我不知道原因。
getSum<<<M,1>>>(M, N, d_in, d_out);
getSum<<<1,M>>>(M, N, d_in, d_out);

調用 CUDA kernel 的語法是kernel<<<numBlocks, threadsPerBlock>>> 因此,第一行提交了一個 kernel,其中包含 M 個 1 個線程的塊。 不要那樣做:這是非常低效的。 事實上, CUDA 編程手冊說:

NVIDIA GPU 架構圍繞可擴展的多線程流式多處理器(SM) 陣列構建。 當主機 CPU 上的 CUDA 程序調用 kernel 網格時,將枚舉網格的塊並將其分發到具有可用執行能力的多處理器。 一個線程塊的線程在一個多處理器上並發執行,多個線程塊可以在一個多處理器上並發執行。 當線程塊終止時,新塊在空出的多處理器上啟動。 [...]
多處理器以 32 個並行線程組(稱為warp )的形式創建、管理、調度和執行線程。 [...]
一個 warp 一次執行一條公共指令,因此當一個 warp 的所有 32 個線程都同意它們的執行路徑時,就可以實現完全的效率 如果 warp 的線程通過依賴於數據的條件分支發散,則 warp 執行所采用的每個分支路徑,禁用不在該路徑上的線程 分支分歧只發生在一個扭曲內; 不同的 warp 獨立執行,無論它們是執行公共的還是不相交的代碼路徑。

結果,第一次調用創建了M個 1 線程塊,浪費了每個 warp 中 32 個可用的 31 個 CUDA 內核。 這意味着您可能只會讀取 GPU 峰值性能的 3%...

第二個調用創建一個M線程塊。 由於M不是 32 的倍數,因此浪費了很少的 CUDA 內核。 此外,它僅使用 1 個 SM,而不是 GPU 上的許多可用模塊,因為您只有一個塊。 現代 GPU 有幾十個 SM(我的 GTX-1660S 有 22 個 SM)。 這意味着您將僅使用 GPU 功能的一小部分(幾 %)。 更不用說 memory 訪問模式不是連續減慢計算速度......

如果您想更有效地使用您的 GPU,您需要提供更多並行性減少資源浪費 您可以從編寫 kernel 開始,在 2D 網格上使用 atomics 執行歸約 這並不完美,但比您的初始代碼要好得多。 您還應該注意連續讀取 memory (共享相同扭曲的線程應該讀取/寫入連續的內存塊)。

在編寫 CUDA 代碼之前,請仔細閱讀CUDA 手冊或教程。 它很好、准確地描述了這一切。


更新:

根據新信息,您正在嘗試使用blockSize的問題可能是由於kernel中的跨步 memory 訪問(更具體地說是N*i )。 跨步 memory 訪問模式很慢,並且當跨度變大時通常會更慢。 在您的 kernel 中,每個線程將訪問 memory 中的不同塊。 如前所述,GPU(實際上是大多數硬件計算單元)已針對訪問連續數據塊進行了優化。 如果你想解決這個問題並獲得更快的結果,你需要在另一個維度上並行工作(所以不是M而是N )。

此外,BLAS 調用效率低下,因為 CPU 上循環的每次迭代都會調用 GPU 上的 kernel。 調用 kernel 會引入相當大的開銷(通常從幾微秒到 ~100 微秒)。 因此,在稱為數萬次的循環中執行此操作將非常慢。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM