導入
CUDA は、GPU 上で実行するための並列コンピューティング フレームワークであり、多くの計算負荷の高いタスクを大幅に高速化できます。CUDA では、カーネルは GPU 上で実行されるプログラム コードであり、メモリ管理は GPU が必要なデータに確実にアクセスできるようにするプロセスです。この記事では、CUDA プログラミングの基本をより深く理解できるように、CUDA のカーネル関数とメモリ管理について紹介します。
1. CUDAカーネル機能
1.1 CUDAカーネル関数の記述
CUDA では、カーネル関数は GPU 上で実行できる特別な関数です。CPU 上の関数とは異なり、カーネル関数は多くのスレッドで同時に実行できるため、GPU は多くの異なるタスクを同時に処理できます。CUDA カーネルは、__global__ 修飾子によって識別される関数であり、関数を GPU で実行するコードにコンパイルするようにコンパイラーに指示します。以下は、単純なベクトル加算カーネルの例です。
__global__ void vecAdd(float *a, float *b, float *c, int n)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
c[i] = a[i] + b[i];
}
}
GPU でカーネル関数を起動するには、カーネル呼び出しと呼ばれる特別な構文を使用する必要があります。以下は起動ベクトル追加カーネルの例です。
1.2 CUDAカーネル機能の起動
vecAdd<<<numBlocks, blockSize>>>(a, b, c, n);
この例では、numBlocks は開始されるスレッド ブロックの数を示し、blockSize は各スレッド ブロックに含めるスレッドの数を示します。この例では、ベクトル サイズ n を使用して、各スレッドが 1 つのベクトル要素を確実に処理できるように、起動する必要があるスレッドの総数を決定しました。
2. CUDAメモリ管理
CUDA メモリ管理には、GPU が必要なデータに確実にアクセスできるようにするプロセスが含まれるため、重要なトピックです。CUDA には、ホスト メモリ、デバイス メモリ、共有メモリという 3 つの異なるメモリ タイプがあります。この記事では、CUDA のメモリ モデルとメモリ管理メカニズムについて詳しく説明します。
2.1 CUDAメモリモデル
CUDA のメモリ モデルは、従来の CPU メモリ モデルとは異なります。CPU では、すべてのメモリが統合されています。つまり、プログラムは任意のメモリ アドレスに直接アクセスできます。GPU ではメモリが複数のレベルに分割されており、メモリの各レベルではアクセス方法と速度が異なります。具体的には、CUDA のメモリ モデルには次のメモリ タイプが含まれます。
- レジスタ: 各スレッドには独自のレジスタがあり、ローカル変数や一時変数などのデータを保存するために使用できます。レジスタはメモリの最も高速な形式ですが、その数は限られており、通常は数千のみです。
- 共有メモリ: すべてのスレッドがアクセスできるメモリ。通常はブロック内に共有データを保存するために使用されます。共有メモリのアクセス速度はグローバル メモリよりも高速ですが、その容量には制限があり、通常は数十 KB のみです。
- グローバル メモリ: すべてのスレッドからアクセスできるメモリ。通常、グローバル変数や入出力データなどのデータを保存するために使用されます。グローバルメモリのアクセス速度は共有メモリに比べて遅いですが、容量は大きく、通常は数ギガバイトです。
- 定数メモリ: プログラム コードや事前定義された定数などの定数データを保存するために使用される読み取り専用メモリ。コンスタント メモリのアクセス速度はグローバル メモリよりも高速ですが、その容量には制限があり、通常は数十 KB のみです。
- テクスチャ メモリ: 画像やテクスチャなどのデータを保存するために使用され、高度な画像処理操作を実装できます。テクスチャ メモリはグローバル メモリよりもアクセスが高速ですが、容量が限られており、通常は数ギガバイトのみです。
- ローカル メモリ: 各スレッドには独自のローカル メモリがあり、スタック フレームや関数のローカル変数などのデータを保存するために使用されます。ローカルメモリはレジスタや共有メモリに比べてアクセス速度は遅くなりますが、容量は大きくなります。
CUDA プログラムでは、さまざまなタイプのメモリを宣言してアクセスするには、特定のキーワードが必要です。一般的に使用されるメモリ キーワードの一部を次に示します。
-
global : デバイス上で実行される関数 (「カーネル関数」とも呼ばれます) を宣言するために使用されます。
-
shared : スレッド ブロック内のすべてのスレッド間で共有できる共有メモリを宣言するために使用されます。
-
device : デバイス上で実行される関数を宣言するために使用されますが、他のデバイス関数によって呼び出されたり、ホスト コードによって呼び出されたりすることはありません。
-
host : ホスト上で実行される関数を宣言するために使用されます。
-
constant : デバイス上の読み取り専用メモリである定数メモリを宣言するために使用されます。
-
strict : ポインターが特定のメモリー領域への唯一のポインターであることを指定するために使用されます。これは、コンパイラーがメモリー・アクセスを最適化するのに役立ちます。
-
マネージド: ホストとデバイスの両方がアクセスできる統合メモリを宣言するために使用されます。これにより、ホスト メモリからデバイス メモリへ、またはその逆のデータの自動転送が可能になります。
-
align : メモリ内の変数の位置合わせを指定するために使用され、メモリ アクセスの最適化に役立ちます。
2.2 CUDA メモリ管理
CUDA は、メモリを管理するためのいくつかの API 関数を提供します。
パラメータ | 説明する |
---|---|
cudaMalloc() | GPU にグローバル メモリを割り当てるために使用されます |
cudaFree() | GPU 上のグローバル メモリを解放するために使用されます |
cudaMemcpy() | ホストとデバイス間でデータをコピーするために使用されます |
cudaMalloc() | GPU 上でカーネルを定義するために使用されます |
cudaMalloc() | 共有メモリ内の変数を宣言するために使用されます |
2.3 コード例
以下は、メモリ管理関数を使用して CUDA プログラムでメモリを割り当て、解放する方法を示す簡単なプログラム例です。
#include <stdio.h>
__global__ void kernel(int *a)
{
int idx = threadIdx.x + blockIdx.x * blockDim.x;
a[idx] = idx;
}
int main()
{
int *a, *dev_a;
int size = 1024 * sizeof(int);
// 分配设备内存
cudaMalloc((void**)&dev_a, size);
// 在设备上运行kernel函数
kernel<<<1, 1024>>>(dev_a);
// 将结果从设备复制到主机
a = (int*)malloc(size);
cudaMemcpy(a, dev_a, size, cudaMemcpyDeviceToHost);
// 打印结果
for (int i = 0; i < 1024; i++)
{
printf("%d\n", a[i]);
}
// 释放设备内存
cudaFree(dev_a);
free(a);
return 0;
}
このプログラムは、cudaMalloc() 関数を使用して、デバイス上に 1024 * sizeof(int) バイトのサイズのメモリを割り当て、kernel() 関数を使用してこのメモリを初期化します。次に、cudaMemcpy() 関数を使用して結果がデバイスからホストにコピーされ、結果が出力されました。最後に、cudaFree() 関数を使用してデバイスのメモリが解放されます。
結論は
この記事では、CUDAのカーネル機能とメモリ管理について紹介しました。カーネル関数は GPU 上で実行するためのプログラム コードであり、多くのスレッドで同時に実行できるため、GPU は CPU よりも同時実行性の高いアプリケーションに適しています。メモリ管理は、プログラマが GPU メモリを効率的に割り当てて管理できるため、CUDA プログラミングにおける重要な側面です。