• <xmp id="om0om">
  • <table id="om0om"><noscript id="om0om"></noscript></table>
  • 3 月 19 日下午 2 點,鎖定 NVIDIA AI 網絡中文專場。立即注冊觀看
    數據中心/云端

    使用 NVIDIA Compute Sanitizer 更有效地調試 CUDA

    ?

    調試代碼是軟件開發的關鍵方面,但可能具有挑戰性且耗時。并行編程可以為已經很復雜的調試過程引入新的維度,其中可以同時處理數千個線程。

    開發人員可以使用各種工具和技術來幫助使調試變得更簡單、更高效。本文介紹了一種調試工具:NVIDIA Compute Sanitizer。我們將探索這些功能,并通過示例向您展示它的用途,以便您可以在調試過程中節省時間和精力,同時提高 CUDA 應用程序的可靠性和性能。

    Compute Sanitizer 隨CUDA Toolkit一起捆綁。

    什么是 Compute Sanitizer ?

    Compute Sanitizer 是一套工具,可以對代碼的功能正確性執行不同類型的檢查。調試的一個關鍵挑戰是找到錯誤的根本原因,解決它通常比追蹤它更容易,尤其是在并行執行環境中,因為在這種環境中,錯誤的來源可能是瞬態的。

    Compute Sanitizer 通過檢查代碼是否存在內存訪問違規、競爭條件、對未初始化變量的訪問以及同步錯誤,擅長于根本原因調試。所有這些都可能表現為 bug ,但其行為不一定會直接導致源代碼中的根本原因。

    您可能已經熟悉一種用于調試的工具:cuda-memcheck。但是,該工具已在 CUDA 11.6 中被棄用,并在 ZCk 12.0 及更高版本中被刪除。Compute Sanitizer 已取代它的位置,提供了額外的功能,如改進的性能和對 Microsoft hardware-accelerated GPU scheduling 的支持,以及對內存檢查之外的功能的更廣泛支持。

    Compute Sanitizer 中有四個主要工具:

    • memcheck:用于內存訪問錯誤和泄漏檢測
    • racecheck:共享內存數據訪問危險檢測工具
    • initcheck:未初始化的設備全局內存訪問檢測工具
    • synccheck:用于線程同步危險檢測

    除了這些工具, Compute Sanitizer 還有一些額外的功能:

    開始使用 Compute Sanitizer

    Compute Sanitizer 是 CUDA Toolkit 的一部分。要了解更多信息和獲取工具包的鏈接,請訪問 NVIDIA Compute Sanitizer

    安裝工具包后,使用以下格式從命令行啟動 Compute Sanitizer :

    $ compute-sanitizer [options] app_name [app_options]

    表 1 顯示了計算消毒器的選項。想要了解更多信息,請參閱 命令行選項,在 Compute Sanitizer 用戶手冊 中。

    選項 描述
    --kernel-regex kns=myKernel 子字符串 控制計算消毒器工具檢查哪些內核。對于管理測試和工具輸出的大型復雜代碼非常有用。
    –-launch-skip N 跳過N內核在開始檢查之前啟動。
    –-log-file 文件名 設置 Compute Sanitizer 寫入的文件。通常, Compute Sanitizer 直接寫入stdout.
    --generate-coredump yes 當檢測到錯誤時創建一個 CUDA 核心轉儲,稍后可以加載到 CUDA debugger 中cuda-gdb以便進一步分析。
    表 1 。 Compute Sanitizer 命令行界面的一些選項

    為 Compute Sanitizer 編譯

    Compute Sanitizer 可以在沒有任何特殊編譯標志的情況下成功分析和檢查 GPU 應用程序。但是,通過在代碼的編譯階段包含一些額外的標志,可以使工具的輸出更加有用,例如-lineinfo生成行號信息,而不會在優化級別上影響代碼。然后 Compute Sanitizer 可以將錯誤歸因于源代碼行。

    計算消毒器內存檢查

    也許 Compute Sanitizer 中最常用的工具是內存檢查器。下面的代碼示例顯示了一個簡單的 CUDA 程序,用于將數組的每個元素乘以標量。這個代碼執行到完全沒有抱怨,但你能看到它有什么問題嗎?

    #include <assert.h>
    #include <stdio.h>
     
    #define N 1023
     
    __global__ void scaleArray(float* array, float value) {
      int threadGlobalID    = threadIdx.x + blockIdx.x * blockDim.x;
      array[threadGlobalID] = array[threadGlobalID]*value;
      return;
    }
     
    int main() {
      float* array;
      cudaMallocManaged(&array, N*sizeof(float)); // Allocate, visible to both CPU and GPU
      for (int i=0; i<N; i++) array[i] = 1.0f;    // Initialize array
     
      printf("Before: Array 0, 1 .. N-1: %f %f %f\n", array[0], array[1], array[N-1]);
      scaleArray<<<4, 256>>>(array, 3.0);
      cudaDeviceSynchronize();
     
      printf("After : Array 0, 1 .. N-1: %f %f %f\n", array[0], array[1], array[N-1]);
      assert(array[N/2] == 3.0); // Check it's worked
      exit(0);
    }

    如果您發現越界數組訪問,則得 10 分:

    • 執行配置<<<4, 256>>>啟動 4 個塊,每個塊中有 256 個線程,因此總共有 1024 個線程。
    • 數組有長度N= 1023 ,索引為 0 , 1 …,N-2 = 1021 ,N-1 = 1022 。
    • 在某個點上, 1024 線程,它有一個threadGlobalID的值1023 = threadIdx.x + blockIdx.x * blockDim.x = 255+3*256,嘗試執行代碼。
    • 嘗試將越界數組訪問作為array[1023].

    這導致了一個令人討厭的錯誤:“未定義的行為”。它很可能會悄無聲息地失敗。在較大的程序中,它可能會導致嚴重的正確性問題,影響其他內存分配,甚至可能導致分段錯誤。

    嘗試編譯并運行以下代碼:

    $ nvcc -lineinfo example1.cu -o example1.exe
    $ ./example1.exe
    Before: Array 0, 1 .. N-1: 1.000000 1.000000 1.000000
    After : Array 0, 1 .. N-1: 3.000000 3.000000 3.000000

    請來 Compute Sanitizer 提供幫助。嘗試運行以下命令,您應該會看到類似的輸出:

    $ compute-sanitizer --tool memcheck ./example1.exe
     
    ========= COMPUTE-SANITIZER
    Before: Array 0, 1 .. N-1: 1.000000 1.000000 1.000000
    ========= Invalid __global__ read of size 4 bytes
    =========     at 0x70 in /home/pgraham/devblog/NCS/example1.cu:8:scaleArray(float *, float)
    =========     by thread (255,0,0) in block (3,0,0)
    =========     Address 0x7f3aae000ffc is out of bounds
    =========     and is 1 bytes after the nearest allocation at 0x7f3aae000000 of size 4092 bytes
    ...

    想要了解更多關于如何解釋此輸出的信息,請參閱 理解 Memcheck 錯誤,但我們可以討論一些關鍵特性。首先,您會得到錯誤 info Invalid __global__ read,因為 GPU 正試圖讀取某個不是合法地址的全局存儲器。然后,您可以獲得文件和行號,以及導致此問題的實際線程和塊。在這種情況下,example1.cu:8 映射到源中的直線 array[threadGlobalID] = array[threadGlobalID]*value;

    現在您可以修復代碼了。有多種選擇,但添加if threadGlobalID<N之前的錯誤線路可能是最容易的。重新編譯并運行memcheck 工具再次確認。

    現在,你發現其他問題了嗎?

    如果你發現缺少,得 20 分cudaFree 對于MallocManaged 數組。同樣,代碼運行到完成。您似乎得到了正確的答案,但由于沒有釋放分配的內存,您引入了泄漏!這可能會減少后續應用程序可用的內存量,甚至導致系統不穩定。

    香草味的memcheck 錯過了這個。如何檢查這些錯誤?的附加選項之一memcheck 該工具可以在以下方面為您提供幫助:--leak-check=full.

    $ compute-sanitizer --tool memcheck --leak-check=full ./example1.exe
     
    ========= COMPUTE-SANITIZER
    Before: Array 0, 1 .. N-1: 1.000000 1.000000 1.000000
    After : Array 0, 1 .. N-1: 3.000000 3.000000 3.000000
    ========= Leaked 4092 bytes at 0x7ff652000000
    =========     Saved host backtrace up to driver entry point at allocation time
    =========     Host Frame: [0x2b7e93]
    =========                in /usr/lib/x86_64-linux-gnu/libcuda.so.1
    =========     Host Frame:__cudart585 [0x439a0]
    =========                in /home/pgraham/devblog/NCS/./example1.exe
    =========     Host Frame:__cudart836 [0x10c76]
    =========                in /home/pgraham/devblog/NCS/./example1.exe
    =========     Host Frame:cudaMallocManaged [0x51483]
    =========                in /home/pgraham/devblog/NCS/./example1.exe
    =========     Host Frame:cudaError cudaMallocManaged<float>(float**, unsigned long, unsigned int) [0xb066]
    =========                in /home/pgraham/devblog/NCS/./example1.exe
    =========     Host Frame:main [0xac2e]
    =========                in /home/pgraham/devblog/NCS/./example1.exe
    =========     Host Frame:__libc_start_main [0x24083]
    =========                in /usr/lib/x86_64-linux-gnu/libc.so.6
    =========     Host Frame:_start [0xab0e]
    =========                in /home/pgraham/devblog/NCS/./example1.exe
    =========
    ========= LEAK SUMMARY: 4092 bytes leaked in 1 allocations
    ========= ERROR SUMMARY: 1 error

    您應該看到類似于代碼示例中所示的輸出。cudaError 突出顯示,這表明您對cudaMallocManaged 創建了泄漏的內存。在代碼退出之前,未釋放分配的內存。正在添加cudaFree(array);就在最后exit(0);修復了這個問題。這樣做,重新編譯、執行并檢查您(以及memcheck 工具)現在對您的代碼感到滿意。

    這是一個簡單的程序,用于在 GPU 上縮放陣列,以顯示 Compute Sanitizer 和 memcheck 如何工作。使用網格步長循環,可以為任意大小的數組編寫代碼,以訪問 CUDA 中的數組。要了解有關圍繞 CUDA API 調用的錯誤檢查代碼的更多信息,請參閱 如何在 CUDA C/C++ 中查詢設備屬性和處理錯誤

    什么是數據競賽?

    數據競賽是并行編程方法特有的問題。當多個線程同時訪問共享數據,并且至少有一個訪問是寫操作時,就會發生這種情況。圖 1 顯示了一個簡單的示例。

    Diagram shows threads A and B performing overlapping operations on values in shared memory so the local values are different and there is a question mark on the final shared value depending on when operations complete.
    圖 1 。具有重疊操作的并行線程的數據競賽示例

    使用聲明的存儲__shared__限定符被放置在片上共享存儲器中。同一線程塊內的所有線程都可以訪問這種按塊共享內存,與全局內存訪問相比速度要快得多。共享內存經常用于線程間通信,并用作保存正在處理的數據的臨時緩沖區。

    考慮線程 A 和線程 B 并行工作,并將它們的本地計數貢獻給共享計數器。線程將它們自己的本地值添加到共享值,并同時將它們的和寫回共享內存。由于 A 和 B 現在正在向同一地址寫入不同的值,因此發生了數據競爭,結果突然不正確,甚至可能是未定義的。

    有一些機制可以避免這種情況。例如,鎖和原子操作通過保護對共享值的更新來幫助確保正確的行為。然而,我們都容易犯錯。在具有數千個線程的復雜代碼中,是否存在問題可能是不明確的。共享值很可能仍然會增加,只是不是按照數據值所建議的方式增加,從而產生一個看似成功的、帶有錯誤值的運行。

    這就是 Compute Sanitizerracecheck功能是如此寶貴。此工具是一個競爭條件檢測功能,可幫助您識別和解決 CUDA 代碼中的數據競爭。

    下面的代碼示例顯示了用于演示的 GPU 內核racecheck:

    #include <assert.h>
    #include <stdio.h>
     
    #define N 1024
     
    __global__ void blockReduceArray(int* array, int* sum) {
      int threadGlobalID = threadIdx.x + blockIdx.x * blockDim.x;
      __shared__ int blockSum;
     
      if (threadIdx.x  == 0 ) {
        sum[blockIdx.x] = 0; // Initialise the return value
        blockSum = 0;        // Initialise our block level counter
      }
      __syncthreads();
     
      // Add each thread's value to our block level total
      blockSum += array[threadGlobalID];
      __syncthreads();
     
      // Set the return value
      if (threadIdx.x  == 0 ) sum[blockIdx.x] = blockSum;
      return;
    }
     
    int main() {
      int globalSum;
      int* sum;
      int* array;
      int numBlocks = 4;
      cudaMallocManaged(&array, N*sizeof(int));
      cudaMallocManaged(&sum, numBlocks*sizeof(int));
      for (int i=0; i<N; i++) array[i] = 1; // Initialize array
     
      blockReduceArray<<<numBlocks, N/numBlocks>>>(array, sum);
      cudaDeviceSynchronize();
     
      // Do a reduction on the host of the block values
      globalSum = 0;
      for (int i=0; i<numBlocks; i++) globalSum += sum[i];
      printf("After kernel - global sum = %d\n", globalSum);
     
      cudaFree(sum);
      cudaFree(array);
      exit(0);
    }

    該示例將數組中的所有值相加以生成單個值,也稱為減少活動它在 GPU 上的塊級別進行匯總。然后,每個塊的總和返回到主機,并再次求和,以返回將數組中的每個值相加的總值。此示例使用快速共享內存作為緩沖區,以保存數組元素添加的運行總數。

    這種方法避免了對全局內存進行不必要的寫入,直到內核結束時進行最終更新。在引入此類優化時,最好使用分析驅動的方法。對代碼進行分析,檢查是否存在任何瓶頸、未充分利用的硬件或要優化的算法;應用您的更改;然后重復。

    在您熟悉了代碼之后,編譯并運行它,看看它是否有效。您正在將數組中的每個元素初始化為一,并且有 1024 個元素,因此最終的總和應該是 1024 。以下是輸出:

    $ nvcc -lineinfo example2.cu -o example2.exe
    $ ./example2.exe
    $
    After kernel - global sum = 4

    另一個錯誤: 4 絕對不是 1024 ,正如你所期望的那樣!

    計算消毒器racecheck 幫助您確定失敗的原因并避免出現這種情況。跑道檢查 命令的執行方式與memcheck下面的示例顯示了該命令的輸出。第 17 行出現問題,如錯誤消息所示。

    $ compute-sanitizer --tool racecheck ./example2.exe
     
    ========= COMPUTE-SANITIZER
    ========= Error: Race reported between Read access at 0xe0 in /home/pgraham/devblog/NCS/example2.cu:17:blockReduceArray(int *, int *)
    =========     and Write access at 0x100 in /home/pgraham/devblog/NCS/example2.cu:17:blockReduceArray(int *, int *) [16 hazards]
    =========
    After kernel - global sum = 4
    ========= RACECHECK SUMMARY: 1 hazard displayed (1 error, 0 warnings)

    如果您查看突出顯示的代碼行,您可以看到問題:

    ...
    // Add each thread's value to the block level total
    blockSum += array[threadGlobalID];
    ...

    塊中的所有線程同時嘗試讀取存儲為blockSum,將它們的數組值添加到其中,并將其寫回共享內存地址。這創建了一個競賽條件,如圖 1 中的示例所示。因此,每個線程讀取共享值( 0 ),將其遞增( 1 ),然后寫回 1 。最終,共享值最終是 1 ,而不是 256 ,當將四個塊中的每一個加在一起時,您會看到錯誤的答案 4 。

    您可以通過將第 17 行更改為 atomicAdd 來實現:

    atomicAdd(&blockSum, array[threadGlobalID]);

    此操作保護對共享值的訪問blockSum 通過確保它是由訪問線程以串行方式讀取、遞增和寫入的。代碼現在可以正常運行。

    順便提一句,atomicAdd 在修復過程中可能會降低代碼性能。例如,它可能會序列化每個塊中的 256 個線程。NVIDIA CUB 是一個可重復使用的軟件組件庫,它提供塊級和設備級原語,用于執行高度優化的縮減操作。

    在可能的情況下,我們建議在開發和性能調優通用代碼模式時使用庫或組件(如 CUB ),因為它們通常會超過您在合理時間內可以實現的性能。而且它們通常是免費的!

    如果不是這樣簡單的代碼知道了預期的答案,那么像這樣的比賽條件很容易被發現。所以racecheck 幫助避免了以后難以破解的問題。

    結論

    使用NVIDIA Compute Sanitizer,立即下載CUDA Toolkit

    希望我們已經向您介紹了如何開始使用 Compute Sanitizer。當然,這些工具的功能非常豐富,我們只是略知一二。想要了解更多關于 Compute Sanitizer 的信息和示例,請訪問 NVIDIA/compute-sanitizer-samples GitHub 樣本回購和 Compute Sanitizer 用戶手冊

    最近的 GTC 課程涵蓋了 Compute Sanitizer 中引入的一些新功能:

    為了獲得支持,開發者論壇以及專門針對 sanitizer 工具的 子論壇都是不錯的起點。

    如果你想更深入地了解本文中沒有討論的任何功能,請告訴我們。祝你好運!

    ?

    +4

    標簽

    人人超碰97caoporen国产