CUDAカーネルのグリッドとブロックの次元を選択するにはどうすればよいですか?


112

これは、CUDAグリッド、ブロック、およびスレッドのサイズを決定する方法に関する質問です。これは、ここに投稿された質問に対する追加の質問です

このリンクをたどると、talonmiesからの回答にコードスニペットが含まれます(以下を参照)。「チューニングとハードウェアの制約によって通常選択される値」というコメントを理解できません。

CUDAのドキュメントで、これを説明する適切な説明や説明が見つかりませんでした。要約すると、私の質問はblocksize、次のコードが与えられたときに最適な(スレッドの数)を決定する方法です。

const int n = 128 * 1024;
int blocksize = 512; // value usually chosen by tuning and hardware constraints
int nblocks = n / nthreads; // value determine by block size and total work
madd<<<nblocks,blocksize>>>mAdd(A,B,C,n);

回答:


148

その答えには2つの部分があります(私が書いた)。1つの部分は定量化が簡単で、もう1つはより経験的です。

ハードウェアの制約:

これは簡単に定量化できる部分です。現在のCUDAプログラミングガイドの付録Fには、カーネルの起動に含めることができるブロックごとのスレッド数を制限する多くのハード制限がリストされています。これらのいずれかを超えると、カーネルは実行されません。それらはおおよそ次のように要約できます。

  1. 各ブロックは合計で512/1024スレッドを超えることはできません(それぞれ計算機能 1.xまたは2.x以降)
  2. 各ブロックの最大寸法は[512,512,64] / [1024,1024,64]に制限されています(Compute 1.x / 2.x以降)
  3. 各ブロックは、合計8k / 16k / 32k / 64k / 32k / 64k / 32k / 64k / 32k / 64kレジスタを超えることはできません(計算1.0、1.1 / 1.2、1.3 / 2.x- / 3.0 / 3.2 / 3.5-5.2 / 5.3 / 6-6.1 / 6.2 / 7.0)
  4. 各ブロックは16kb / 48kb / 96kbを超える共有メモリを消費できません(Compute 1.x / 2.x-6.2 / 7.0)

これらの制限内にある場合、正常にコンパイルできるカーネルはすべてエラーなしで起動します。

性能調整:

これは経験的な部分です。上記のハードウェア制約内で選択したブロックあたりのスレッド数は、ハードウェアで実行されるコードのパフォーマンスに影響を与える可能性があり、実際に影響します。各コードの動作は異なり、それを定量化する唯一の実際の方法は、慎重なベンチマークとプロファイリングによるものです。しかし、もう一度、非常に大まかに要約します:

  1. ブロックあたりのスレッド数は、現在のすべてのハードウェアで32のワープサイズの倍数にする必要があります。
  2. GPUの各ストリーミングマルチプロセッサユニットには、アーキテクチャのさまざまなメモリと命令パイプラインのレイテンシをすべて十分に隠し、最大のスループットを実現するのに十分なアクティブワープが必要です。ここでの正統なアプローチは、最適なハードウェア占有率を達成しようとすることです(Roger Dahlの回答が参照しているもの)。

2番目のポイントは、誰もが単一のStackOverflow回答でそれをカバーしようとするつもりはないだろうと思う大きなトピックです。問題の側面の定量分析について博士論文を書いている人がいます(UCバークレーのVasily Volkovによるこのプレゼンテーションと、トロント大学のHenry Wongによるこの論文は、質問の実際の複雑さの例について参照してください)。

エントリレベルでは、選択したブロックサイズ(上記の制約によって定義された有効なブロックサイズの範囲内)は、コードの実行速度に影響を与える可能性があり、実際に影響を及ぼしますが、ハードウェアによって異なります。あなたが持っているコードとあなたが実行しているコード ベンチマークにより、ほとんどの重要なコードはブロック範囲ごとに128〜512スレッドの「スイートスポット」を持っていることがわかりますが、それがどこにあるかを見つけるには、ユーザー側での分析が必要です。良いニュースは、ワープサイズの倍数で作業しているため、サーチスペースが非常に有限であり、特定のコードの最適な構成が比較的見つけやすいことです。


2
「ブロックあたりのスレッド数はワープサイズの丸めの倍数である必要があります」これは必須ではありませんが、そうでない場合はリソースを浪費します。ブロックが多すぎるカーネルの起動後にcudaErrorInvalidValueがcudaGetLastErrorによって返されることに気づきました(compute 2.0は10億のブロックを処理できず、compute 5.0は処理できないようです)-したがって、ここにも制限があります。
masterxilo 2016年

4
Vasili Volkovリンクは無効です。彼の2010年9月:低稼働率でのパフォーマンスの向上に関する記事(現在nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdfにあります)が気に入ったと思います。ここにコード付きのbitbucketがあります:bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

上記の回答は、ブロックサイズがパフォーマンスにどのように影響するかを示しており、占有率の最大化に基づいて選択するための一般的なヒューリスティックを提案しています。ブロックサイズを選択する基準を提供する必要ありませんが、CUDA 6.5(現在のリリース候補バージョン)には、占有率の計算と起動構成を支援するいくつかの新しいランタイム関数が含まれていることに言及する価値があります。

CUDA Proのヒント:占有APIが起動構成を簡素化

便利な関数の1つは、cudaOccupancyMaxPotentialBlockSize最大占有率を達成するブロックサイズをヒューリスティックに計算することです。その関数によって提供される値は、起動パラメーターの手動最適化の開始点として使用できます。以下は小さな例です。

#include <stdio.h>

/************************/
/* TEST KERNEL FUNCTION */
/************************/
__global__ void MyKernel(int *a, int *b, int *c, int N) 
{ 
    int idx = threadIdx.x + blockIdx.x * blockDim.x; 

    if (idx < N) { c[idx] = a[idx] + b[idx]; } 
} 

/********/
/* MAIN */
/********/
void main() 
{ 
    const int N = 1000000;

    int blockSize;      // The launch configurator returned block size 
    int minGridSize;    // The minimum grid size needed to achieve the maximum occupancy for a full device launch 
    int gridSize;       // The actual grid size needed, based on input size 

    int* h_vec1 = (int*) malloc(N*sizeof(int));
    int* h_vec2 = (int*) malloc(N*sizeof(int));
    int* h_vec3 = (int*) malloc(N*sizeof(int));
    int* h_vec4 = (int*) malloc(N*sizeof(int));

    int* d_vec1; cudaMalloc((void**)&d_vec1, N*sizeof(int));
    int* d_vec2; cudaMalloc((void**)&d_vec2, N*sizeof(int));
    int* d_vec3; cudaMalloc((void**)&d_vec3, N*sizeof(int));

    for (int i=0; i<N; i++) {
        h_vec1[i] = 10;
        h_vec2[i] = 20;
        h_vec4[i] = h_vec1[i] + h_vec2[i];
    }

    cudaMemcpy(d_vec1, h_vec1, N*sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vec2, h_vec2, N*sizeof(int), cudaMemcpyHostToDevice);

    float time;
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start, 0);

    cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, MyKernel, 0, N); 

    // Round up according to array size 
    gridSize = (N + blockSize - 1) / blockSize; 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Occupancy calculator elapsed time:  %3.3f ms \n", time);

    cudaEventRecord(start, 0);

    MyKernel<<<gridSize, blockSize>>>(d_vec1, d_vec2, d_vec3, N); 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Kernel elapsed time:  %3.3f ms \n", time);

    printf("Blocksize %i\n", blockSize);

    cudaMemcpy(h_vec3, d_vec3, N*sizeof(int), cudaMemcpyDeviceToHost);

    for (int i=0; i<N; i++) {
        if (h_vec3[i] != h_vec4[i]) { printf("Error at i = %i! Host = %i; Device = %i\n", i, h_vec4[i], h_vec3[i]); return; };
    }

    printf("Test passed\n");

}

編集

cudaOccupancyMaxPotentialBlockSizeで定義されcuda_runtime.hたファイルと、次のように定義されています。

template<class T>
__inline__ __host__ CUDART_DEVICE cudaError_t cudaOccupancyMaxPotentialBlockSize(
    int    *minGridSize,
    int    *blockSize,
    T       func,
    size_t  dynamicSMemSize = 0,
    int     blockSizeLimit = 0)
{
    return cudaOccupancyMaxPotentialBlockSizeVariableSMem(minGridSize, blockSize, func, __cudaOccupancyB2DHelper(dynamicSMemSize), blockSizeLimit);
}

パラメータの意味は次のとおりです

minGridSize     = Suggested min grid size to achieve a full machine launch.
blockSize       = Suggested block size to achieve maximum occupancy.
func            = Kernel function.
dynamicSMemSize = Size of dynamically allocated shared memory. Of course, it is known at runtime before any kernel launch. The size of the statically allocated shared memory is not needed as it is inferred by the properties of func.
blockSizeLimit  = Maximum size for each block. In the case of 1D kernels, it can coincide with the number of input elements.

CUDA 6.5以降、APIが提案する1Dブロックサイズから自分の2D / 3Dブロックの寸法を計算する必要があることに注意してください。

また、CUDAドライバーAPIには、占有計算のための機能的に同等のAPIが含まれてcuOccupancyMaxPotentialBlockSizeいるため、上記の例のランタイムAPIで示したのと同じ方法でドライバーAPIコードで使用することもできます。


2
2つの質問があります。まず、手動で計算したgridSizeよりもグリッドサイズをminGridSizeとして選択する必要がある場合。次に、「その関数によって提供される値は、起動パラメーターの手動最適化の開始点として使用できます。」-起動パラメーターを手動で最適化する必要があることを意味しますか?
nurabha

2D / 3Dブロックの寸法の計算方法に関するガイダンスはありますか?私の場合、2Dブロックの寸法を探しています。乗算して元のブロックサイズを計算するときに、x係数とy係数を計算するだけのケースですか?
Graham Dawes

1
@GrahamDawes これは興味深いかもしれません。
Robert Crovella、2015

9

ブロックサイズは通常、「占有率」を最大化するように選択されます。詳細については、CUDA占有率を検索してください。特に、CUDA Occupancy Calculatorスプレッドシートを参照してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.