(4) CUDA環境のインストールとプログラミング

1. インストールバージョンを確認する

        1. 対応する CUDA インストール パッケージをダウンロードするには、グラフィックス カードでサポートされている最も高い CUDA バージョンを確認します。

NVIDIA グラフィックス カード ドライバーをインストールし、現在のシステムの NVIDIA グラフィックス カードに関する詳細情報を表示します: nvidia-smi、

「CUDA バージョン」または「コンピューティング機能」セクションを調べて、グラフィックス カードでサポートされている表示された CUDA バージョン番号を見つけます。
        2. CUDA のバージョンに対応する cuDNN のバージョンを確認します。CUDA → オンラインで検索します (NVIDIA が提供する公式ドキュメントまたはリリース ノートを参照することをお勧めします)。

        3. ツールキット (nvidia): CUDA 完全ツール インストール パッケージ。Nvidia ドライバー、CUDA プログラムの開発に関連する開発ツール キット、およびその他のインストール オプションを提供します。CUDAプログラムのコンパイラ、IDE、デバッガなどをはじめ、CUDAプログラムに対応する各種ライブラリファイルとそのヘッダファイル。

2. 設置方法

公式サイトからダウンロードしてインストールし、cuDNNをダウンロードします。

 3. CUDA環境変数の設定


上記の nvcc-v コマンドでエラーが表示された場合は、次のコマンドを環境変数構成ファイル ~/.bashrc (環境内の CUDA バージョンが 12.0 であると仮定) に入力し、
次の 2 行を追加する必要があります。export
PATH =$PATH: /usr/local/cuda-12.0/bin
エクスポート LD ライブラリ パス=$LD ライブラリ パス:/usr/local/cuda-12.0/1ib64

        NVCC (NVIDIA CUDA Compiler) は、CUDA コードをコンパイルするために NVIDIA が提供するコンパイラーです。CUDA C/C++ コードを GPU で実行できるバイナリ コードに変換し、GPU アクセラレーション コンピューティングを可能にします。NVCC は単なるコンパイラではなく、コンパイルとビルドのプロセスを管理するためのいくつかのオプションも提供します。
        NVCC は CUDA コードと通常の C/C++ コードの混合コンパイルをサポートしており、開発者はホスト (CPU) コードとデバイス (GPU) コードを同じファイルに同時に記述することができます。開発者は、CUDA 拡張 C/C++ 構文を使用して、CUDA カーネル関数、スレッドおよびブロック制御などを含むデバイス コードを作成できます。

4. NVCC コンパイル オプション

​​​​​​​

 多くのデバイス オプションは使用されません。

5. NVCC を使用して単純な CUDA プログラムをコンパイルする

1. CUDA ソース コードを作成する
. CUDA コードを含むソース コード ファイルを作成します。通常、CUDA コードには、ホスト コード (CPU 上で実行) とデバイス コード (GPU 上で実行) が含まれます。
2. nvcc を使用してコンパイルする
3. 実行可能ファイルを実行する: 生成された実行可能ファイルを使用して CUDA プログラムを実行する
4. プログラムのパフォーマンス分析を実行する 

6. test-checkdeviceinfo のコーディング 

#include<iostream>
#include<cuda.h>
#include<cuda_runtime.h>
int main() {
int dev = 0;
cudaDeviceProp devProp;
cudaGetDeviceProperties(&devProp, dev);
std::cout << "GPU Device Name" << dev << ": " << 
devProp.name << std::endl;
std::cout << "SM Count: " << 
devProp.multiProcessorCount << std::endl;
std::cout << "Shared Memory Size per Thread Block: " << 
devProp.sharedMemPerBlock / 1024.0 << " KB" << std::endl;
std::cout << "Threads per Thread Block: " << 
devProp.maxThreadsPerBlock << std::endl;
std::cout << "Threads per SM: " << 
devProp.maxThreadsPerMultiProcessor << std::endl;
std::cout << "Warps per SM: " << 
devProp.maxThreadsPerMultiProcessor / 32 << std::endl;
return 0;
}
// Kernel定义
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) 
{ 
int i = blockIdx.x * blockDim.x + threadIdx.x; 
int j = blockIdx.y * blockDim.y + threadIdx.y; 
if (i < N && j < N) 
C[i][j] = A[i][j] + B[i][j]; 
}
int main() 
{ 
...
// Kernel 线程配置
 dim3 threadsPerBlock(16, 16); 
dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
// kernel调用
 MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C); 
...
}

コーディング実験~シリアルコードとの違い(機材編)

CUDA プログラミングでは、threadIdx、blockIdx、blockDim、および GridDim は、並列計算におけるスレッドおよびスレッド ブロックのインデックスと次元を決定するために使用される組み込み変数です。GPU 上のスレッド ブロックとスレッドを操作するため。

 threadIdx.x、threadIdx.y、および threadIdx.z は、それぞれ、スレッド ブロック内の x、y、z 方向の現在のスレッドのインデックスを表します。

各スレッド ブロック内のスレッドには独自の threadIdx があります。 

 blockIdx.x、blockIdx.y、および blockIdx.z は、グリッド内の x、y、z 方向の現在のスレッド ブロックのインデックスを表します。各スレッド ブロックには独自の blockIdx があります。 

 blockDim.x、blockDim.y、および blockDim.z は、スレッド ブロック内のスレッド数の x、y、z 次元を表します。

これは固定値であり、ほとんどの状況で同じになります。

GridDim.x、gridDim.y、gridDim.z は、グリッド全体の x、y、z 方向の寸法、つまりグリッド内のスレッド ブロックの数を表します。これはグリッド全体で同じ固定値です。

7. CUDAプログラム性能検出ツール-nvprof

        nvprof は、NVIDIA が提供するコマンド ライン ツールで、GPU 使用率、メモリ帯域幅、実行時間、遅延など、CUDA アプリケーションに関するパフォーマンス インジケーター データを収集するために使用できます。さらに、nvprofi は、GPU コード実行中の CUDA APIi 呼び出しをトレースして、パフォーマンスのボトルネックを引き起こす可能性のある関数とコード パスを特定できます。
        nvprofj は、CUDA アプリケーションのパフォーマンスのボトルネックをより深く理解するのに役立つ、タイムライン ビュー、関数概要ビュー、命令分析ビューなどのさまざまな分析オプションを提供します。

シンプルなタイムラインビュー
 nvprof./my_cuda_app  

統計情報の指定: GPU 使用率、占有率

 nvprof./my_cuda_appnvprof --metrics

gpu_utilization,achieved_occupancy ./my_cuda_app

出力ファイルを指定します
nvprof --output-profile
my_profile.nvvp ./my_cuda_app

分析ビューを使用します
nvprof --analysis-metrics -o
my_profile.nvvp./my_cuda_app 

8. ブロックとスレッドのインデックスのマッピング (行列関連)

 通常、行列は行優先方式でホストメモリに線形に格納されますが、CUDAプログラムでは2次元グリッド(2,3)+2次元ブロック(4,2)を作成することができ、ブロックとスレッドのインデックスを使用し、マッピング行列のインデックスを使用します。

加算の場合、結果行列の (i, j) は、2 つの n * m 行列の対応する座標 (i, j) の加算であり、合計 n * m 回の加算演算が実行され、合計 n * m 個のスレッドが必要です。つまり、少なくとも開かれるスレッドの数は N = n * m です。

乗算の場合、結果行列の (i, j) は、最初の行列の i 行目 (n * k) と 2 番目の行列の j 列目 (k * m) の要素の合計です。 - 各スレッドはタスクを完了する必要があります。スレッドの総数 N = n * m。

N (n * m) が得られると、blockDim を設定できます (上限は 1024 ですが、通常は 32 の整数倍です)。システムは自動的に GridDim を生成します。

通常書かれるC言語では、(i,j)はi行j列を表します。

行列座標 (ix, iy) は、iy 行目、ix 列目を表します。つまり、x 軸と y 軸と同じ座標系 (x は水平、y は垂直) を表します。

`blockIdx.x` は CUDA プログラミングの組み込み変数で、現在のスレッド ブロックの x 方向のインデックスを表すために使用されます。

CUDA では、スレッドはスレッド ブロックの形式でグリッドに編成されます。各スレッド ブロックは複数のスレッドで構成され、スレッド ブロックは 3 次元のグリッドに編成されます。`blockIdx.x` は、x 方向の現在のスレッド ブロックのインデックス、つまりグリッド全体におけるその位置を表します。

「blockIdx.x」を通じて、CUDA プログラム内で条件判断や計算を実行し、異なるスレッド ブロックに異なる操作を実行させたり、異なるデータにアクセスさせたりすることができます。

int ix = blockIdx.x * blockDim.x + threadIdx.x; 

int iy = blockIdx.y * blockDim.y + threadIdx.y;

このコードは、2D グリッド内の現在のスレッドのグローバル インデックスを計算します。で:

- `blockIdx.x` と `blockIdx.y` は、それぞれ x 方向と y 方向の現在のスレッド ブロックのインデックスを表します。
- `blockDim.x` と `blockDim.y` は、それぞれ各スレッド ブロック内のスレッドの数 (x 方向と y 方向) を表します。
- `threadIdx.x` と `threadIdx.y` は、それぞれ、そのスレッドが属するスレッド ブロック内の現在のスレッドのインデックス (x 方向と y 方向) を表します。

これらの値から、グリッド全体の現在のスレッドのグローバル インデックスを計算できます。

具体的には、「ix」は次のように計算されます: 現在のスレッドの x 座標は「blockIdx.x * blockDim.x + threadIdx.x」です。

同様に、「iy」は次のように計算されます: 現在のスレッドの y 座標は「blockIdx.y * blockDim.y + threadIdx.y」です。

この方法で計算された `ix` と `iy` は、CUDA プログラムのさまざまなスレッドからのデータにアクセスして処理するために使用できます。

9. スレッドブロック内のスレッドの数

スレッド ブロック内のスレッドの数は、32 の倍数として構成するのが最適です。これは、GPU のハードウェア特性と並列コンピューティング モデルの設計によって決まります。

GPU はスレッド ブロックの単位で並列コンピューティング タスクを実行し、各スレッド ブロック内のスレッドは連携してデータと通信を共有できます。GPU がコンピューティング タスクを実行するとき、スレッド ブロックは小さなスレッド グループ (ワープ) に分割され、各スレッド グループには一連の連続したスレッドが含まれます。

GPU のハードウェアは、スレッド グループ (ワープ) の単位でスケジュールおよび実行されます。具体的には、クロック サイクルで、GPU は各スレッドを個別にスケジュールして実行するのではなく、実行するスレッド グループ (ワープ) を選択します。同じスレッド グループ (ワープ) 内のスレッドは、同じ命令ストリームで実行する必要があります。これは SIMT (単一命令、複数スレッド) 実行モデルと呼ばれます。

これにより、スレッド グループ (ワープ) 内のスレッド数が固定されており、通常は 32 であるため、スレッド ブロック内のスレッド数は 32 の倍数として構成するのが最適であることが決まります (特定の数は、GPU アーキテクチャによって異なる場合があります)。 スレッド ブロック内のスレッドの数が 32 の倍数ではない場合、スレッド グループ間で負荷の不均衡 (ワープ) が発生します。たとえば、スレッド ブロック内のスレッドの数が 40 の場合、40 個のスレッドを含む 1 つのスレッド グループ (ワープ) と、8 つのスレッドのみを含む別のスレッド グループ (ワープ) が存在することになり、GPU コンピューティング リソースが無駄になります。

したがって、GPU の並列コンピューティング機能を最大限に活用し、スレッド グループの負荷バランスを維持するには、スレッド ブロック内のスレッド数を 32 の倍数に構成するのが最適です。これにより、すべてのスレッド グループ (ワープ) が同じクロック サイクルでスケジュールされ、実行されるようになり、コンピューティング パフォーマンスが向上します。

ただし、実際の作業では、手動でパラメータを設定して作成されるスレッドの数と、並列ループに必要なスレッドの数が一致しない可能性があります (例: 実際には 1230 のループを実行する必要がありますが、通常は 2048 のスレッドが設定されます) 。

1. スレッドの総数が実際の作業に必要な数を超えるように構成パラメータを設定します。

2. カーネル関数にパラメータを渡すときは、処理されるデータ セットの合計サイズ、または作業を完了するために必要なスレッドの合計数を表す N を渡します。

3. グリッド内のスレッド インデックスを計算した後 (threadIdx + blockIdx*blockDim を使用)、インデックスが N を超えているかどうかを判断し、N を超えていない場合にのみカーネル関数に関連する作業を実行します。

注: 作業の総量 N とスレッド ブロック内のスレッドの数がわかっている場合に適用されます。

// 假设N是已知的
int N = 100000;
// 把每个block中的thread数设为256
size_t threads_per_block = 256;
// 根据N和thread数量配置Block数量
size_t number_of_blocks = (N + threads_per_block - 1) / threads_per_block;
//传入参数N
some_kernel<<<number_of_blocks, threads_per_block>>>(N);

ブロック数 = (N + ブロックごとのスレッド - 1) / ブロックごとのスレッド;

この文は N/threads_per_block を切り上げます。

切り上げの利点は、スレッドの数が N 以上である必要があることです。つまり、N 個のタスクを実行する必要性を満たし、最大でも 1 ブロック内のスレッド数を無駄にできなければなりません。

10. カーネル機能 

カーネル関数を定義します。

カーネルファンクションとは、並列計算においてGPU上で実行される関数のことを指します。CUDA プログラミング モデルでは、カーネル関数は開発者によって記述され、GPU 上で実行される並列コンピューティング タスク コードです。

カーネル関数は `__global__` 修飾子によって CUDA で識別され、パラメータと戻り値を受け入れることができます。カーネル関数では、特定の構文と関数を使用して、スレッド インデックス、スレッド ブロック、グリッドを使用して並列実行を管理するなど、並列コンピューティングの方法を指定できます。

CUDA プログラミングでは、カーネル関数が複数回呼び出され、異なるスレッドで実行され、各スレッドが同じ計算タスクを独立して実行します。カーネル関数を実行するとき、スレッド インデックス、スレッド ブロック インデックス、グリッド インデックスなどの情報を使用して、計算における各スレッドの役割とタスクを決定できます。

カーネル関数は通常、ベクトル加算、行列乗算などの集中的な数値計算タスクを実行するために使用され、GPU の並列計算機能を最大限に活用して計算パフォーマンスを向上させることができます。適切なカーネル関数を記述することで、コンピューティング タスクを複数の並列スレッド ブロックに分割し、GPU 上で同時に実行できるため、コンピューティング プロセスが高速化されます。

GPU と CPU は 2 つの独立したコンピューティング デバイスであるため、カーネル関数は CPU 上の関数を直接呼び出したり、CPU メモリ内のデータにアクセスしたりすることはできないことに注意してください。カーネル関数でCPU上のデータを使用する必要がある場合は、ホスト(CPU)のメモリからデバイス(GPU)のメモリにデータをコピーし、カーネル関数でデバイスのメモリにアクセスする必要があります。同様に、計算結果をデバイス メモリからホスト メモリにコピーして戻す必要がある場合、対応するデータ転送操作も必要になります。

カーネル関数とは、一言で言えば GPU 上で実行される並列計算タスクコードであり、GPU の並列計算能力を最大限に活用することで、負荷の高い数値計算タスクを高速化できます。

分布を計算する:

タスクをスレッド ブロックとスレッドに割り当てます。問題のサイズと GPU のパフォーマンスに応じて、ブロックの数と各ブロックのスレッドの数を決定します。

データアクセスと同期:

複数のスレッドが互いに干渉しないようにしてください。カーネル関数内では、共有メモリを使用してグローバル メモリ アクセスの数を減らし、パフォーマンスを向上させます。スレッド間の同期の問題に注意する必要があります。特に共有メモリを使用する場合は、__syncthreads() を使用してスレッドを同期します。

11. リダクションアルゴリズム 

リダクション アルゴリズムは、より大きな問題をより小さなサブ問題に変換し、サブ問題の結果をマージすることで元の問題の解を得るために使用される一般的な並列計算アルゴリズムです。

並列コンピューティングでは、通常、リダクション アルゴリズムを使用して、データ セット内の要素に対して合計、最大化、最小化などの集計操作を実行します。リダクション アルゴリズムは、データ セットを複数の部分に分割し、並列計算のためにそれらを異なる処理ユニット (スレッド、スレッド ブロック、プロセッサ コアなど) に割り当て、各部分の結果をマージして最終結果を得ることで機能します。

ここでは、例として合計演算を使用した、一般的なリダクション アルゴリズムの例を示します。

1. 入力データセットを複数の小さな部分に均等に分割し、それらを異なる処理ユニットに割り当てます。
2. 各処理ユニットは、割り当てられた部分の局所和を計算します。
3. 各処理ユニットはローカル合計を加算してグローバル合計を取得します。
4. すべての部分が 1 つの結果に結合されるまで、上記の手順を繰り返します。

この削減アルゴリズムは、問題のサイズを繰り返し半分にすることで実装でき、それによって高い並列パフォーマンスが実現されます。各反復では、問題がより小さな部分に分割され、各部分に対してリダクション操作が実行されます。このようにして、各反復のサイズは半分になりますが、並列計算のサイズは増加します。

リダクション アルゴリズムは、多くのアプリケーション、特に並列コンピューティングおよび並列プログラミング モデル (CUDA、OpenMP など) で広く使用されています。コンピューティング効率を向上させ、複数のプロセッシングユニットの並列コンピューティング機能を最大限に活用することで、問題解決プロセスを加速します。

リダクションアルゴリズムのパフォーマンスは、データの分割と結合の方法、並列計算の負荷分散、通信オーバーヘッドなどの要因に密接に関係していることに注意してください。リダクション アルゴリズムを設計および実装するときは、アルゴリズムのスケーラビリティと効率を確保するために、これらの要素を考慮する必要があります。

 最適化削減アルゴリズム

・ワープの区別を避ける:ワープ内のスレッドが判定文の異なる分岐を実行する場合、分岐条件を満たすスレッドはその分岐のコマンドを実行し、分岐条件を満たさないスレッドはアイドル状態となりスキップできませこのようにして、スレッド全体のワープを実行します。

行効率は分岐がない場合の半分になります。

·連続アドレス読み取り: CUDA でのスレッドのデータの連続読み取りは他の方法よりも効率的であるため、スレッドのアドレス指定方法を連続に変更できます。

·ループ展開: 操作のボトルネックは、アドレス指定とループ自体にある可能性があります。複数の削減ステップをループに組み合わせて、制御フローのオーバーヘッドを削減します。

マルチステップ削減: 複数の削減ステージを使用して、連続した削減操作を実行します。これらのステージでは、さまざまなメソッドとパラメーターを使用してデータのサイズを徐々に縮小し、並列処理をより効果的に活用できます。

分岐条件を満たさないスレッドがアイドル状態になり、スキップできない理由は次のとおりです。

GPU 並列コンピューティングでは、スレッド ワープは連続したスレッドのグループであり、通常は 32 個のスレッドが含まれます。これらのスレッドは同じ命令を同時に実行しますが、データが異なる場合があります。スレッドワープ内のスレッドが判定文の異なる分岐を実行する場合、分岐条件を満たすスレッドは対応する分岐の命令を実行し、条件を満たさないスレッドは実行する必要がありません。

GPU アーキテクチャのスレッド ワープは SIMD (単一命令複数データ) モードで動作するため、つまり 1 つの命令がスレッド ワープ内のすべてのスレッドに同時に作用するため、分岐ステートメントを実行すると、条件を満たさないスレッドが条件をスキップすることはできません。これは GPU ハードウェア設計によって決定され、各スレッドは命令パイプラインのリズムに従って実行する必要があり、特定の命令を実行するかスキップするかを個別に選択することはできません。

ワープ内のスレッドが異なるブランチを実行すると、条件を満たさないスレッドはアイドル状態になります。これは、実質的なコンピューティング タスクを実行せず、他のスレッドが対応するブランチの命令実行を完了するのを待つだけであることを意味します。これが発生すると、一部のスレッドには実行すべき有効な作業がないため、ワープのスループットが低下します。

スレッド ワープの差別化を回避し、GPU 使用率を向上させるために、いくつかの最適化戦略を採用できます。たとえば、合理的にコードを記述し、分岐ステートメントの発生を回避するように努めたり、データの再配置、データのプリフェッチ、その他のテクノロジを使用して、スレッド ワープ内のスレッドのワークロードを増やし、アイドル時間を短縮したりします。さらに、GPU プログラミングでは、ワープの微分による影響を軽減するために、より大きなワープ サイズ (64 スレッドや 128 スレッドなど) を使用できます。

おすすめ

転載: blog.csdn.net/weixin_48060069/article/details/132311796