cudaMalloc()
とcudaMemcpy()
を使用して線形メモリを割り当ててコピーする方法については、かなり理解しています。ただし、CUDA関数を使用して2Dまたは3D行列を割り当て、コピーする場合、特に2D/3D配列を扱うときに常に存在するピッチポインターに関して、さまざまな引数に戸惑うことがよくあります。ドキュメントは、それらの使用方法に関するいくつかの例を提供するのに適していますが、私はパディングとピッチの概念に精通していることを前提としていますが、私はそうではありません。
私は通常、ドキュメントまたはWeb上の他の場所で見つけたさまざまな例を微調整することになりますが、その後に続くブラインドデバッグは非常に苦痛なので、私の質問は次のとおりです。
ピッチとは?どうやって使うの? CUDAで2Dおよび3D配列を割り当ててコピーするにはどうすればよいですか?
ここでは、CUDAでのピッチポインターとパディングについて説明します。
最初に、非線形メモリが存在する理由から始めましょう。 cudaMallocを使用してメモリを割り当てると、結果はmallocを使用した割り当てのようになり、指定されたサイズの連続したメモリチャンクがあり、必要なものを何でも入れることができます。 10000 floatのベクトルを割り当てる場合は、次のようにします。
_float* myVector;
cudaMalloc(&myVector, 10000*sizeof(float));
_
そして、古典的なインデックス付けによってmyVectorのi番目の要素にアクセスします。
_float element = myVector[i];
_
次の要素にアクセスする場合は、次のようにします。
_float next_element = myvector[i+1];
_
最初の要素のすぐ隣にある要素にアクセスするのは(私が気づいていないので、今のところはなりたくないので)費用がかからないため、非常にうまく機能します。
メモリを2D配列として使用すると、状況が少し異なります。 10000 floatベクトルが実際に100x100配列であるとしましょう。同じcudaMalloc関数を使用して割り当てることができます。i番目の行を読み取りたい場合は、次のようにします。
_float* myArray;
cudaMalloc(&myArray, 10000*sizeof(float));
int row[100]; // number of columns
for (int j=0; j<100; ++j)
row[j] = myArray[i*100+j];
_
したがって、メモリをmyArray + 100 * iからmyArray + 101 * i-1に読み取る必要があります。実行されるメモリアクセス操作の数は、この行が使用するメモリワードの数によって異なります。メモリWordのバイト数は、実装によって異なります。単一の行を読み取るときにメモリアクセスの数を最小限に抑えるには、Wordの先頭から行を開始する必要があるため、新しい行の先頭まで、すべての行のメモリを埋め込む必要があります。
配列をパディングするもう1つの理由は、共有メモリアクセスに関するCUDAのバンクメカニズムです。アレイが共有メモリ内にある場合、アレイはいくつかのメモリバンクに分割されます。 2つのCUDAスレッドは、同じメモリバンクに属するメモリにアクセスしない限り、同時にアクセスできます。通常、各行を並行して処理したいので、新しい行の先頭に各行を埋め込むことで、確実にそれにアクセスできるようにします。
ここで、cudaMallocを使用して2D配列を割り当てる代わりに、cudaMallocPitchを使用します。
_size_t pitch;
float* myArray;
cudaMallocPitch(&myArray, &pitch, 100*sizeof(float), 100); // width in bytes by height
_
ここでのピッチは関数の戻り値であることに注意してください。cudaMallocPitchはシステム上での値を確認し、適切な値を返します。 cudaMallocPitchが行うことは次のとおりです。
最後に、各行はピッチのサイズであり、w*sizeof(float)
のサイズではないため、通常、必要以上のメモリを割り当てました。
しかし今、列の要素にアクセスしたいときは、次のようにする必要があります。
_float* row_start = (float*)((char*)myArray + row * pitch);
float column_element = row_start[column];
_
2つの連続する列の間のバイト単位のオフセットは、配列のサイズから推定できなくなります。そのため、cudaMallocPitchによって返されるピッチを維持します。また、ピッチはパディングサイズの倍数(通常、Wordサイズとバンクサイズの最大値)であるため、効果的に機能します。わーい。
CudaMallocPitchによって作成された配列内の単一の要素を作成してアクセスする方法がわかったので、線形の有無にかかわらず、その一部全体を他のメモリとの間でコピーしたい場合があります。
Mallocを使用してホストに割り当てられた100x100配列に配列をコピーするとします。
_float* Host_memory = (float*)malloc(100*100*sizeof(float));
_
CudaMemcpyを使用する場合は、各行間の埋め込みバイトを含めて、cudaMallocPitchで割り当てられたすべてのメモリをコピーします。メモリのパディングを回避するために必要なことは、各行を1つずつコピーすることです。手動で行うことができます:
_for (size_t i=0; i<100; ++i) {
cudaMemcpy(Host_memory[i*100], myArray[pitch*i],
100*sizeof(float), cudaMemcpyDeviceToHost);
}
_
または、CUDA APIに、itsの利便性のためにパディングバイトを使用して割り当てたメモリから有用なメモリのみを必要とすることを伝えることができます。自動的にごちゃごちゃしてしまうのはとてもいいことだと思います。ありがとうございます。そしてここにcudaMemcpy2Dに入ります:
_cudaMemcpy2D(Host_memory, 100*sizeof(float)/*no pitch on Host*/,
myArray, pitch/*CUDA pitch*/,
100*sizeof(float)/*width in bytes*/, 100/*heigth*/,
cudaMemcpyDeviceToHost);
_
これでコピーが自動的に行われます。幅(ここでは100xsizeof(float))、高さの時間(ここでは100)で指定されたバイト数をコピーし、毎回pitchバイトをスキップします次の行にジャンプします。パディングされる可能性があるため、宛先メモリのピッチも提供する必要があることに注意してください。ここではそうではないので、ピッチは非埋め込み配列のピッチと同じです。これは行のサイズです。 memcpy関数の幅パラメーターはバイト単位で表されますが、高さパラメーターは要素数で表されることにも注意してください。これは、コピーが行われる方法が原因で、上記の手動コピーを書いたように、幅は行に沿った各コピーのサイズ(メモリ内で隣接する要素)であり、高さはこの操作が必要な回数です達成される。 (これらの単位の不一致は、物理学者として、私をとても困らせます。)
3D配列は2D配列と実際に違いはなく、追加のパディングは含まれていません。 3D配列は、パディングされた行の2Dclassical配列です。これが、3D配列を割り当てるときに、行に沿った連続するポイントとのバイト数の差である1つのピッチしか得られない理由です。奥行き次元に沿って連続するポイントにアクセスする場合は、ピッチに列数を安全に乗算して、slicePitchを取得できます。
3DメモリにアクセスするためのCUDA APIは、2Dメモリの場合とは少し異なりますが、考え方は同じです。