GPU メモリ アーキテクチャ -- グローバル メモリ ローカル メモリ レジスタ ヒープ 共有メモリ コンスタント メモリ テクスチャ メモリ

ここに画像の説明を挿入
上の表は、さまざまな記憶のさまざまな特性を表しています。スコープ列は、プログラムのどの部分がこのメモリを使用できるかを定義します。有効期間は、このメモリ内のデータがプログラムから認識できる期間を定義します。これに加えて、メモリ アクセスを高速化するために、GPU プログラムで L1 および L2 キャッシュを使用することもできます。
つまり、すべてのスレッドにレジスタ ファイルがあり、これが最も高速です。共有メモリにはブロック内のスレッドのみがアクセスできますが、グローバル メモリ ブロックよりもはるかにアクセスしやすくなっています。グローバル メモリは最も遅いですが、すべてのブロックからアクセスできます。

グローバルメモリ

すべてのブロックはグローバル メモリに対して読み書きできます。このメモリは低速ですが、コード内のどこからでも読み書きできます。キャッシュにより、グローバル メモリへのアクセスが高速化されます。cudaMalloc 経由で割り当てられたすべてのメモリはグローバル メモリです。次の簡単なコードは、プログラムからグローバル メモリを使用する方法を示しています。

#include <stdio.h>
#define N 5

__global__ void gpu_global_memory(int *d_a)
{
    
    
	// "array" is a pointer into global memory on the device
	d_a[threadIdx.x] = threadIdx.x;
}

int main()
{
    
    
	// Define Host Array
	int h_a[N];
	//Define device pointer	
	int *d_a;       
						
	cudaMalloc((void **)&d_a, sizeof(int) *N);
	// now copy data from host memory to device memory 
	cudaMemcpy((void *)d_a, (void *)h_a, sizeof(int) *N, cudaMemcpyHostToDevice);
	// launch the kernel 
	gpu_global_memory << <1, N >> >(d_a);  
	// copy the modified array back to the host memory
	cudaMemcpy((void *)h_a, (void *)d_a, sizeof(int) *N, cudaMemcpyDeviceToHost);
	printf("Array in Global Memory is: \n");
	//Printing result on console
	for (int i = 0; i < N; i++) 
	{
    
    
		printf("At Index: %d --> %d \n", i, h_a[i]);
	}

	return 0;
}

ローカルメモリとレジスタファイル

ローカル メモリとレジスタ ファイルは各スレッドに固有です。レジスタは、各スレッドが使用できる最速のメモリです。カーネルで使用される変数がレジスタ ファイルに収まらない場合、変数はローカル メモリに格納されます。これはレジスタ オーバーフローと呼ばれます。ローカル メモリを使用する場合は 2 つのケースがあることに注意してください。1 つは十分なレジスタがない場合、もう 1 つはローカル配列の添字に無限のインデックスが付けられている場合など、レジスタにまったく配置できない場合です。基本的に、ローカル メモリはスレッドごとのグローバル メモリの唯一の部分であると考えることができます。ローカル メモリはレジスタ ファイルよりもはるかに低速です。ローカル メモリは L1 キャッシュと L2 キャッシュを通じてバッファリングされますが、レジスタ オーバーフローがプログラムのパフォーマンスに影響を与える可能性があります。
簡単なプログラムを以下に示します。

#include <stdio.h>
#define N 5

__global__ void gpu_local_memory(int d_in)
{
    
    
	int t_local;    
	t_local = d_in * threadIdx.x;     
	printf("Value of Local variable in current thread is: %d \n", t_local);
}

int main()
{
    
    

	printf("Use of Local Memory on GPU:\n");
	gpu_local_memory << <1, N >> >(5);  
	cudaDeviceSynchronize();
	return 0;
}

コード内の t_local 変数は各スレッドに対してローカルに一意であり、レジスタ ファイルに格納されます。この変数で計算する場合、計算速度が最も速くなります。

共有メモリ

共有メモリはチップ内にあるため、グローバル メモリよりもはるかに高速です。(CUDA のメモリ速度には 2 つの側面があり、1 つは低レイテンシ、もう 1 つは広い帯域幅です。ここでは特に低レイテンシを指します。) キャッシュされていないグローバル メモリ アクセスと比較して、共有メモリのレイテンシは約 100 分の 1 です。同じブロック内のスレッドは、同じ共有メモリにアクセスできます (注: 異なるブロック内のスレッドから見える共有メモリの内容は異なります)。これは、多くのスレッドが結果を他のスレッドと共有する必要があるアプリケーションで役立ちます。プログラム内で。ただし、同期していないと混乱や誤った結果が生じる可能性もあります。共有メモリへの書き込みが完了する前に、あるスレッドの計算結果を他のスレッドが読み込むとエラーとなります。したがって、メモリアクセスは適切に制御または管理される必要があります。これは _syncthreads() 命令によって行われ、プログラムの実行を続行する前にメモリへのすべての書き込みが確実に完了します。これはバリアとも呼ばれます。バリアの意味は、ブロック内のすべてのスレッドがそのコード行に到達し、そこで他のスレッドが完了するのを待つことです。すべてのスレッドがここに到着すると、引き続き一緒に実行できます。共有メモリとスレッド同期の使用を示すために、MA の計算例をここに示します。

#include <stdio.h>
#include <cuda.h>
#include <cuda_runtime.h>

__global__ void gpu_shared_memory(float *d_a)
{
    
    
	// Defining local variables which are private to each thread
	int i, index = threadIdx.x;
	float average, sum = 0.0f;

	//Define shared memory
	__shared__ float sh_arr[10];
	sh_arr[index] = d_a[index];
	__syncthreads();    // This ensures all the writes to shared memory have completed

	for (i = 0; i<= index; i++) 
	{
    
     
		sum += sh_arr[i]; 
	}
	average = sum / (index + 1.0f);
	d_a[index] = average; 
}

int main(int argc, char **argv)
{
    
    
	//Define Host Array
	float h_a[10];   
	//Define Device Pointer
	float *d_a;       
	
	for (int i = 0; i < 10; i++)
	{
    
    
		h_a[i] = i;
	}
	// allocate global memory on the device
	cudaMalloc((void **)&d_a, sizeof(float) * 10);
	// now copy data from host memory  to device memory 
	cudaMemcpy((void *)d_a, (void *)h_a, sizeof(float) * 10, cudaMemcpyHostToDevice);
	
	gpu_shared_memory << <1, 10 >> >(d_a);
	// copy the modified array back to the host memory
	cudaMemcpy((void *)h_a, (void *)d_a, sizeof(float) * 10, cudaMemcpyDeviceToHost);
	printf("Use of Shared Memory on GPU:  \n");
	//Printing result on console
	for (int i = 0; i < 10; i++) 
	{
    
    
		printf("The running average after %d element is %f \n", i, h_a[i]);
	}
	return 0;
}

MA 演算は非常に単純です。配列内の現在の要素の前にあるすべての要素の平均値を計算します。多くのスレッドは計算時に配列内の同じデータを使用します。これは共有メモリを使用するための理想的な使用例であり、グローバル メモリよりも高速なデータ アクセスが可能になります。これにより、スレッドごとのグローバル メモリ アクセスの数が減り、プログラムの待ち時間が短縮されます。共有メモリ上の数値または変数は、__shared__ 修飾子を使用して定義されます。この例では、共有メモリ上に 10 個の float 要素を持つ配列を定義します。一般に、共有メモリのサイズは、ブロックあたりのスレッド数と同じである必要があります。10 (要素) の配列を扱っているため、共有メモリのサイズもこの大きさになるように定義します。
次のステップでは、グローバル メモリから共有メモリにデータをコピーします。各スレッドは独自のインデックスを介して要素をコピーするため、ブロック全体でデータのコピー操作が完了し、データが共有メモリに書き込まれます。次の行では、共有メモリ配列からの読み取りを開始しますが、続行する前に、すべての (スレッド) が書き込み操作を完了していることを確認する必要があります。そこで、__syncthreads() を使用して同期を実行してみましょう。
次に、(各スレッドは)(読み取り後に)共有メモリに保存されている値を使用して、for ループを通じて現在の要素の平均値を(最初の要素から)計算し、各スレッドに対応する結果を保存しますin グローバル メモリ内の対応する場所。

一定の記憶

CUDA プログラマは、別のタイプのメモリ、つまり定メモリを使用することがよくあります。NVIDIA GPU カードは、論理的にユーザーに 64 KB の定メモリ空間を提供し、カーネルの実行中に必要な定データを保存するために使用できます。定数メモリには、特定の状況で少量のデータにアクセスする場合にグローバル メモリよりも優れた利点があります。定数メモリを使用すると、グローバル メモリの帯域幅消費もある程度削減されます。このサブセクションでは、CUDA で定数メモリを使用する方法を見ていきます。単純なプログラムを使用して、a * x + b (a と b は定数) の数学演算を実行します。プログラム コードは次のとおりです。

#include "stdio.h"
#include <iostream>
#include <cuda.h>
#include <cuda_runtime.h>

//Defining two constants
__constant__ int constant_f;
__constant__ int constant_g;
#define N	5

//Kernel function for using constant memory
__global__ void gpu_constant_memory(float *d_in, float *d_out) 
{
    
    
	//Thread index for current kernel
	int tid = threadIdx.x;	
	d_out[tid] = constant_f*d_in[tid] + constant_g;
}

int main() 
{
    
    
	//Defining Arrays for host
	float h_in[N], h_out[N];
	//Defining Pointers for device
	float *d_in, *d_out;
	int h_f = 2;
	int h_g = 20;
	// allocate the memory on the cpu
	cudaMalloc((void**)&d_in, N * sizeof(float));
	cudaMalloc((void**)&d_out, N * sizeof(float));
	//Initializing Array
	for (int i = 0; i < N; i++) 
	{
    
    
		h_in[i] = i;
	}
	//Copy Array from host to device
	cudaMemcpy(d_in, h_in, N * sizeof(float), cudaMemcpyHostToDevice);
	//Copy constants to constant memory
	cudaMemcpyToSymbol(constant_f, &h_f, sizeof(int), 0, cudaMemcpyHostToDevice);
	cudaMemcpyToSymbol(constant_g, &h_g, sizeof(int));

	//Calling kernel with one block and N threads per block
	gpu_constant_memory << <1, N >> >(d_in, d_out);
	//Coping result back to host from device memory
	cudaMemcpy(h_out, d_out, N * sizeof(float), cudaMemcpyDeviceToHost);
	//Printing result on console
	printf("Use of Constant memory on GPU \n");
	for (int i = 0; i < N; i++) 
	{
    
    
		printf("The expression for input %f is %f\n", h_in[i], h_out[i]);
	}
	//Free up memory
	cudaFree(d_in);
	cudaFree(d_out);
	return 0;
}

定数メモリ内の変数は、__constant__ キーワードで修飾されます。前のコードでは、2 つの浮動小数点数 constant_f と constant_g が、カーネルの実行中に変更されない定数として定義されています。2 番目に注意すべきことは、(カーネルの外部で) __constant__ を使用して定義した後は、カーネル内で再度定義しないでください。カーネル関数はこれら 2 つの定数を使用して単純な数学演算を実行します。メイン関数では、これら 2 つの定数の値を特別な方法で定数メモリに渡します。
main 関数では、2 つの定数 h_f と h_g がホスト上で定義および初期化され、デバイス上の定数メモリにコピーされます。cudaMemcpyToSymbol 関数を使用して、これらの定数をカーネルの実行に必要な定数メモリにコピーします。この関数には 5 つのパラメータがあります: 最初のパラメータは (書き込まれる) ターゲット、つまり __constant__ で定義した h_f または h_g 定数、2 番目のパラメータはソース ホスト アドレス、3 番目のパラメータは送信サイズ、そして4 番目のパラメータは書き込みターゲットのオフセットで、ここでは 0 です。5 番目のパラメータはデバイスからホストへのデータ送信方向です。最後の 2 つのパラメータはオプションなので、省略する場合は後で 2 番目の cudaMemcpyToSymbol 関数を呼び出します。

テクスチャメモリ

テクスチャ メモリは、データ アクセスに特定のパターンがある場合にプログラムの実行を高速化し、ビデオ メモリの帯域幅を削減できるもう 1 つの ROM です。定数メモリと同様に、これもチップ内にキャッシュされます。このメモリはもともとグラフィック描画用に設計されましたが、汎用コンピューティングにも使用できます。このタイプのメモリは、プログラムが空間的に非常に近接したメモリにアクセスする場合に非常に効率的になります。空間的近接とは、各スレッドの読み取り位置が他のスレッドの読み取り位置に隣接していることを意味します。これは、4 つの隣接する相関点または 8 つの隣接する相関点を処理する必要がある画像処理アプリケーションに非常に役立ちます。
汎用のグローバル メモリ キャッシュでは、この空間的近接性を効果的に処理できず、大量のビデオ メモリ読み取り転送が発生する可能性があります。テクスチャ ストレージはこのメモリ アクセス モデルを利用するように設計されており、ビデオ メモリから 1 回だけ読み取られてバッファリングされるため、実行速度が大幅に速くなります。テクスチャ メモリは 2D および 3D テクスチャの読み取り操作をサポートしますが、CUDA プログラムでテクスチャ メモリを使用することは、特にプログラミングの専門家ではない人にとってはそれほど簡単ではありません。このセクションでは、テクスチャ ストレージを介して配列の割り当てを行う方法の例を説明します。

#include "stdio.h"
#include <iostream>
#include <cuda.h>
#include <cuda_runtime.h>

#define NUM_THREADS 10
#define N 10
texture <float, 1, cudaReadModeElementType> textureRef;

__global__ void gpu_texture_memory(int n, float *d_out)
{
    
    
	int idx = blockIdx.x*blockDim.x + threadIdx.x;
	if (idx < n)
	{
    
    
		float temp = tex1D(textureRef, float(idx));
		d_out[idx] = temp;
	}
}

int main()
{
    
    
	//Calculate number of blocks to launch
	int num_blocks = N / NUM_THREADS + ((N % NUM_THREADS) ? 1 : 0);
	//Declare device pointer
	float *d_out;
	// allocate space on the device for the result
	cudaMalloc((void**)&d_out, sizeof(float) * N);
	// allocate space on the host for the results
	float *h_out = (float*)malloc(sizeof(float) * N);
	//Declare and initialize host array
	float h_in[N];
	for (int i = 0; i < N; i++) 
	{
    
    
		h_in[i] = float(i);
	}
	//Define CUDA Array
	cudaArray *cu_Array;
	cudaMallocArray(&cu_Array, &textureRef.channelDesc, N, 1);
	//Copy data to CUDA Array
	cudaMemcpyToArray(cu_Array, 0, 0, h_in, sizeof(float)*N, cudaMemcpyHostToDevice);
	
	// bind a texture to the CUDA array
	cudaBindTextureToArray(textureRef, cu_Array);
	//Call Kernel	
  	gpu_texture_memory << <num_blocks, NUM_THREADS >> >(N, d_out);
	
	// copy result back to host
	cudaMemcpy(h_out, d_out, sizeof(float)*N, cudaMemcpyDeviceToHost);
	printf("Use of Texture memory on GPU: \n");
	for (int i = 0; i < N; i++)
	{
    
    
		printf("Texture element at %d is : %f\n",i, h_out[i]);
	}
	free(h_out);
	cudaFree(d_out);
	cudaFreeArray(cu_Array);
	cudaUnbindTexture(textureRef);	
}

「テクスチャ参照」を通じてテクスチャピッキングが可能なテクスチャメモリのセグメントを定義します。テクスチャ参照は、texture<> 型の変数を通じて定義されます。定義時には 3 つのパラメータがあります。1 つ目は、texture<> 型の変数が定義されるときのパラメータで、テクスチャ要素のタイプを記述するために使用されます。この例では、タイプは float で、2 番目のパラメータはテクスチャ参照のタイプ (1D、2D、または 3D) を指定します。この例では、これは 1D テクスチャ参照であり、3 番目のパラメータは読み取りモードであり、読み取り時に自動型変換を実行するかどうかを示すオプションのパラメータです。テクスチャ参照がグローバル静的変数として定義されていることを確認し、他の関数に引数として渡せないことも確認してください。このカーネル関数では、各スレッドは、スレッド ID をインデックス位置として使用するデータをテクスチャ参照を通じて読み取り、d_out ポインタが指すグローバル メモリにコピーします。
main 関数では、ビデオ メモリ上のメモリと配列を定義して割り当てた後、ホスト上の配列 (配列内の要素) が 0 ~ 9 の値に初期化されます。この例では、初めて CUDA 配列が使用されています。これらは通常の配列に似ていますが、テクスチャに固有のものです。CUDA 配列はカーネル関数に対して読み取り専用です。ただし、前のコードでわかるように、cudaMemcpyToArray 関数を通じてホスト上に書き込むことができます。cudaMemcpyToArray 関数の 2 番目と 3 番目のパラメータの 0 は、転送対象の CUDA 配列の水平オフセットと垂直オフセットを表します。両方向のオフセット О は、転送がターゲット CUDA 配列の左上隅 (0, 0) から開始されることを意味します。CUDA 配列のメモリ レイアウトはユーザーには不透明で、テクスチャのフェッチ用に特に最適化されています。
cudaBindTextureToArray 関数は、テクスチャ参照を CUDA 配列にバインドします。以前に書き込んだ CUDA 配列は、このテクスチャ参照のバッキング ストアになります。テクスチャ参照バインディングが完了したら、カーネルを呼び出します。カーネルはテクスチャをフェッチし、結果のデータをビデオ メモリ内のターゲット配列に書き込みます。注: CUDA には、ビデオ メモリ内の大量のデータを保存するための 2 つの一般的な保存方法があり、1 つはポインタによって直接アクセスできる通常のリニア ストレージです。もう 1 つは CUDA 配列です。これはユーザーにとって不透明で、カーネル内のポインターによって直接アクセスできませんが、テクスチャまたはサーフェスの対応する関数を通じてアクセスする必要があります。この例のカーネルでは、テクスチャ参照からの読み取りには対応するテクスチャ フェッチ関数が使用され、書き込みは通常のポインタ (d_out[]) を使用して直接実行されます。カーネルの実行が完了すると、結果の配列がホスト上のメモリにコピーされて戻され、コンソール ウィンドウに表示されます。テクスチャ ストレージの使用が完了したら、バインド解除コードを実行する必要があります。これは、cudaUnbindTexture 関数を呼び出すことによって行われます。次に、cudaFreeArray() 関数を使用して、割り当てられたばかりの CUDA 配列スペースを解放します。

おすすめ

転載: blog.csdn.net/taifyang/article/details/128512323