ほとんどのエンジニアは、CPU コードを書き始めたときから CPU とシーケンシャル プログラミングに精通しているため、CPU とシーケンシャル プログラミングに精通しています。ただし、GPU の内部動作や GPU の特徴についてはほとんどわかっていません。過去 10 年間、GPU はディープラーニングで広く使用されてきたため、非常に重要になってきました。したがって、すべてのソフトウェア エンジニアはその基本的な動作原理を理解する必要があります。この記事は、この点に関する背景知識を読者に提供することを目的としています。
この記事の著者はソフトウェア エンジニアの Abhinav Upadhyay であり、この記事の内容の大部分は、GPU アーキテクチャと実行モデルを紹介した『Massive Parallel Processor Programming』(Hwu et al.) の第 4 版に基づいて書かれています。もちろん、この記事で説明する GPU プログラミングの基本的な概念と方法は、他のベンダーの製品にも適用できます。
(この記事はOneFlowにより編集・公開されています。転載の許可についてはご連絡ください。原文: https://codeconfessions.substack.com/p/gpu-computing)
著者 | アビナブ・ウパディヤイ
OneFlow コンパイル
翻訳|ワン・ジリン、ヤン・ティン
1
CPU と GPU を比較する
まず、GPU の開発状況をよりよく理解するために CPU と GPU を比較しますが、1 つのセクションですべてを説明するのは難しいため、これは別のトピックとして扱う必要があります。したがって、いくつかの重要なポイントを説明します。
CPU と GPU の主な違いは、その設計目標です。CPU は逐次命令を実行するように設計されています [1]。長年にわたり、シーケンシャル実行パフォーマンスを向上させるために、CPU 設計に多くの機能が導入されてきました。CPU が一連の命令をできるだけ早く実行できるように、命令実行レイテンシを短縮することに重点が置かれています。これらの機能には、(ほんの数例を挙げると) 命令パイプライン、アウトオブオーダー実行、投機的実行、およびマルチレベル キャッシュが含まれます。
GPU は大規模な並列処理と高スループットを実現するように設計されていますが、この設計では中程度から高い命令レイテンシーが発生します。この設計の方向性は、ビデオ ゲーム、グラフィックス処理、数値計算、そして現在では深層学習での広範な使用の影響を受けており、これらのすべてで広範な線形代数と数値計算を非常に高速に実行する必要があります。これらのデバイスのスループット。
具体的な例を考えてみましょう。CPU は命令レイテンシーが低いため、GPU よりも速く 2 つの数値を加算できます。このような複数の計算を順番に実行する場合、CPU は GPU よりも速く計算を完了できます。ただし、そのような計算を数百万、さらには数十億回実行する必要がある場合、GPU はその強力な大規模並列機能により、CPU よりも速くこれらのコンピューティング タスクを完了します。
これを具体的なデータを通じて説明できます。数値計算におけるハードウェアのパフォーマンスは、1 秒あたりの浮動小数点演算 (FLOPS) で測定されます。NVIDIA の Ampere A100 は、32 ビット精度で 19.5 TFLOPS のスループットを備えています。比較すると、Intel の 24 コア プロセッサのスループットは、32 ビット精度でわずか 0.66 TFLOPS (2021) です。同時に、時間の経過とともに、GPU と CPU の間のスループット性能の差は年々拡大しています。
以下の図は、CPU と GPU のアーキテクチャを比較しています。
図 1: CPU と GPU のチップ設計の比較。『CUDA C++ プログラミングガイド』(NVIDIA)より引用
図に示すように、CPU は、大規模なキャッシュ、少数の算術論理ユニット (ALU)、およびより多くの制御ユニットなど、命令レイテンシを短縮するために主にチップ分野で使用されます。対照的に、GPU は計算能力とスループットを最大化するために多数の ALU を使用し、キャッシュと制御ユニットには非常に小さなチップ領域のみを使用し、これらのコンポーネントは主に CPU 遅延を削減するために使用されます。
耐レイテンシーと高スループット
おそらく、GPU がどのようにして高い遅延を許容し、同時に高いパフォーマンスを提供できるのかに興味があるかもしれません。GPU は、多数のスレッドと膨大な計算能力によってこれを可能にします。単一の命令のレイテンシが高い場合でも、GPU はスレッドの実行を効率的にスケジュールし、いつでもコンピューティング能力を活用できるようにします。たとえば、一部のスレッドが命令の結果を待機している場合、GPU は待機していない他のスレッドの実行に切り替えます。これにより、GPU 上の計算ユニットが常に最大能力で実行され、高いスループットが提供されます。これについては、後でカーネルが GPU 上でどのように実行されるかを説明するときに、より明確に理解できるようになります。
2
GPUアーキテクチャ
GPU が高スループットを達成するのに適していることはすでにわかっていますが、これを達成するために GPU はどのように設計されているのでしょうか? このセクションではこれについて説明します。
GPUコンピューティングアーキテクチャ
GPU は一連のストリーミング マルチプロセッサ (SM) で構成され、各 SM は複数のストリーミング プロセッサ、コア、またはスレッドで構成されます。たとえば、NVIDIA H100 GPU には 132 個の SM があり、それぞれに 64 コアがあり、合計 8448 コアになります。
各 SM には、すべてのコアによって共有される、共有メモリまたは一時メモリと呼ばれる一定量のオンチップ メモリがあります。同様に、SM 上のコントロール ユニット リソースもすべてのコアで共有されます。さらに、各 SM には、スレッドを実行するためのハードウェア ベースのスレッド スケジューラが装備されています。
さらに、各 SM には、GPU によって処理されるワークロードの特定の要件を満たすために、いくつかの機能ユニット、またはテンソル コアやレイ トレーシング ユニットなどの他の高速化されたコンピューティング ユニットが装備されています。
図 2: GPU コンピューティング アーキテクチャ
次に、GPU メモリについて掘り下げて詳細を理解しましょう。
GPUメモリアーキテクチャ
GPU にはさまざまな種類のメモリの複数の層があり、それぞれが特定の目的を持っています。以下の図は、GPU 内の SM のメモリ階層を示しています。
図 3: コーネル大学仮想ワークショップに基づく GPU メモリ アーキテクチャ
分析してみましょう:
-
レジスタ: レジスタから始めましょう。GPU の各 SM には多数のレジスタがあります。たとえば、NVIDIA の A100 および H100 モデルには、SM ごとに 65536 個のレジスタがあります。これらのレジスタはコア間で共有され、スレッドの要求に基づいて動的に割り当てられます。実行中、各スレッドには、他のスレッドが読み書きできないプライベート レジスタが割り当てられます。
-
定数キャッシュ: 次はチップ上の定数キャッシュです。これらのキャッシュは、SM で実行されるコードで使用される定数データをキャッシュするために使用されます。これらのキャッシュを利用するには、プログラマは、GPU がオブジェクトをキャッシュして定数キャッシュに保存できるように、コード内でオブジェクトを定数として明示的に宣言する必要があります。
-
共有メモリ: 各 SM には共有メモリまたは一時メモリもあります。これは、SM 上で実行されているスレッド ブロックが共有して使用する、小型、高速、低遅延のオンチップ プログラマブル SRAM メモリです。共有メモリの設計上の考え方は、複数のスレッドが同じデータを処理する必要がある場合、1 つのスレッドだけがグローバル メモリからデータをロードする必要があり、他のスレッドがこのデータを共有するというものです。共有メモリを適切に使用すると、グローバル メモリから重複データをロードする必要性が減り、カーネルの実行パフォーマンスが向上します。共有メモリは、スレッド ブロック内のスレッド間の同期メカニズムとしても使用できます。
-
L1 キャッシュ: 各 SM には L1 キャッシュもあり、頻繁にアクセスされるデータを L2 キャッシュからキャッシュできます。
-
L2 キャッシュ: すべての SM は L2 キャッシュを共有します。これは、頻繁にアクセスされるデータをグローバル メモリにキャッシュして遅延を短縮するために使用されます。L1 キャッシュと L2 キャッシュは SM に公開されていることに注意してください。つまり、SM はデータを L1 から取得するのか、L2 から取得するのかを知りません。SM はグローバル メモリからデータをフェッチします。これは、CPU での L1/L2/L3 キャッシュの動作と似ています。
-
グローバル メモリ: GPU には、大容量で高帯域幅のダイナミック ランダム アクセス メモリ (DRAM) であるオフチップ グローバル メモリもあります。たとえば、NVIDIA H100 には 80 GB の高帯域幅メモリ (HBM) が搭載されており、帯域幅は 3000 GB/秒です。SM からの距離が長いため、グローバル メモリのレイテンシは非常に高くなります。ただし、チップ上には、この遅延をマスクするのに役立つ多数のコンピューティング ユニットと同様に、いくつかの追加のメモリ層があります。
GPU ハードウェアの主要コンポーネントを理解したところで、コードを実行するときにこれらのコンポーネントがどのように作用するかを詳しく見て理解しましょう。
3
GPUの実行モデルを理解する
GPU がカーネルを実行する方法を理解するには、まずカーネルとは何か、およびその構成を理解する必要があります。
CUDA カーネルとスレッド ブロックの概要
CUDA は、GPU で実行されるプログラムを作成するために NVIDIA が提供するプログラミング インターフェイスです。CUDA では、GPU 上で実行したい計算を C/C++ 関数に似た形式で表現します。この関数はカーネルと呼ばれます。カーネルは、関数の引数として提供される数値のベクトルに対して並列に動作します。簡単な例は、ベクトル加算を実行するカーネルです。つまり、2 つのベクトルを入力として受け取り、それらを要素ごとに加算し、その結果を 3 番目のベクトルに書き込みます。
GPU 上でカーネルを実行するには、複数のスレッドを有効にする必要があります。これらのスレッドは総称してグリッドと呼ばれますが、グリッドにはさらに多くの構造があります。グリッドは 1 つ以上のスレッド ブロック (単にブロックと呼ばれることもあります) で構成され、各スレッド ブロックは 1 つ以上のスレッドで構成されます。
スレッド ブロックとスレッドの数は、データのサイズと必要な並列度によって異なります。たとえば、ベクトル加算の例では、256 次元のベクトルを加算する場合、256 個のスレッドを含む単一スレッド ブロックを構成して、各スレッドがベクトルの 1 つの要素を処理できるようにすることができます。データが大きい場合、GPU で使用できるスレッドが不足する可能性があり、各スレッドが複数のデータ ポイントを処理できる必要がある場合があります。
図 4: スレッド ブロック グリッド。『CUDA C++ プログラミングガイド』(NVIDIA)より引用
カーネルを作成するには 2 つの手順が必要です。最初のステップは、CPU 上で実行されるホスト コードです。コードのこの部分は、データのロード、GPU へのメモリの割り当て、構成されたスレッド グリッドを使用したカーネルの起動に使用されます。2 番目のステップは、デバイス (GPU) の書き込みです。 GPU 上で実行されるコード。
ベクタ追加例のホストコードを次の図に示します。
図 5: 2 つのベクトルを追加するための CUDA カーネルのホスト コード。
下の図は、実際のカーネル関数を定義するデバイス コードを示しています。
図 6: ベクトル加算カーネル定義を含むデバイス コード。
この記事の焦点は CUDA を教えることではないため、このコードについてはこれ以上詳しく説明しません。次に、GPU 上でカーネルを実行する具体的な手順を見てみましょう。
4
GPU上でカーネルを実行する手順
1. ホストからデバイスにデータをコピーする
カーネルの実行をスケジュールするには、必要なすべてのデータをホスト (CPU) メモリから GPU のグローバル メモリ (デバイス メモリ) にコピーする必要があります。それにもかかわらず、最新の GPU ハードウェアでは、統合仮想メモリを使用してホスト メモリからデータを直接読み取ることもできます (論文「EMOGI: Efficient Memory-access for Out-of-memory Graph-traversal in GPUs」を参照)。
2. SM でのスレッド ブロックのスケジューリング
GPU は、必要なデータをすべてメモリ内に格納すると、スレッド ブロックを SM に割り当てます。同じブロック内のすべてのスレッドは、同じ SM によって同時に処理されます。これを行うには、GPU は、スレッドの実行を開始する前に、これらのスレッド用に SM 上のリソースを予約する必要があります。実際の動作では、複数のスレッド ブロックを同じ SM に割り当てて並列実行を実現できます。
図 7: SM へのスレッド ブロックの割り当て
SM の数は限られており、大規模なカーネルには多数のスレッド ブロックが含まれる可能性があるため、すべてのスレッド ブロックをすぐに実行用に割り当てることができるわけではありません。GPU は割り当ておよび実行されるスレッド ブロックのリストを保持しており、いずれかのスレッド ブロックが完了すると、GPU はリストから実行するスレッド ブロックを選択します。
3. シングルインストラクションマルチスレッド(SIMT)とスレッドワープ(Warp)
ご存知のとおり、ブロック内のすべてのスレッドは同じ SM に割り当てられます。ただし、その後、スレッドはさらにサイズ 32 のグループ (warp[2] と呼ばれます) に分割され、実行のために処理ブロックと呼ばれるコアのコレクションにまとめて割り当てられます。
SM は、同じ命令をフェッチしてすべてのスレッドに発行することにより、ワープ内のすべてのスレッドを同時に実行します。これらのスレッドは、データの異なる部分に対して同時に命令を実行します。ベクトル加算の例では、ワープ内のすべてのスレッドが加算命令を実行している可能性がありますが、それらはベクトルの異なるインデックスで動作します。
複数のスレッドが同じ命令を同時に実行するため、このワープの実行モデルは単一命令マルチスレッド (SIMT) とも呼ばれます。これは、CPU の単一命令複数データ (SIMD) 命令に似ています。
Volta とその後の世代の GPU では、独立スレッド スケジューリングと呼ばれる、命令スケジューリングに代わるメカニズムが導入されました。これにより、ワープによる制限を受けることなく、スレッド間の完全な同時実行が可能になります。独立したスレッド スケジューリングにより、実行リソースをより効率的に利用できるようになり、スレッド間の同期メカニズムとしても機能します。この記事では独立したスレッドのスケジューリングについては説明しませんが、CUDA プログラミング ガイドで詳細を学ぶことができます。
4. ワープ スケジュールと遅延許容度
ワープの仕組みについては、議論する価値のある興味深いことがいくつかあります。
SM 内のすべての処理ブロック (コア グループ) がワープを処理しているにもかかわらず、特定の瞬間にアクティブに命令を実行しているのはそのうちの少数だけです。SM で使用できる実行ユニットの数は限られているためです。
一部の命令は実行に時間がかかるため、ワープは命令の結果を待つことになります。この場合、SM は待機中のワープをスリープし、結果を待つ必要のない別のワープを実行します。これにより、GPU は利用可能なすべてのコンピューティング リソースを最大限に活用し、スループットを向上させることができます。
計算オーバーヘッドゼロのスケジューリング: 各ワープの各スレッドには独自のレジスターのセットがあるため、SM があるワープの実行を別のワープに切り替えるときに追加の計算オーバーヘッドはありません。
CPU上のプロセス間のコンテキスト切り替え方式(context-switching)とは異なります。プロセスが長時間実行される操作を待機する必要がある場合、CPU はその間にそのコア上で別のプロセスをスケジュールします。ただし、CPU でのコンテキスト スイッチングは、CPU がレジスタの状態をメイン メモリに保存し、他のプロセスの状態を復元する必要があるため、コストがかかります。
5. 結果データをデバイスからホストメモリにコピーします
最後に、カーネルのすべてのスレッドが実行を終了すると、最後のステップは結果をホスト メモリにコピーして戻すことです。
一般的なカーネルの実行についてはすべて説明しましたが、もう 1 つ議論する価値のある点があります。それは、動的なリソースの分割です。
5
リソースの分割と占領の概念
GPU リソースの使用率は、SM に割り当てられたワープの数と SM がサポートできるワープの最大数との比率を表す「占有」と呼ばれるメトリクスを通じて測定されます。最大のスループットを達成するには、占有率を 100% にする必要があります。ただし、実際にはさまざまな制約があるため、これを達成するのは簡単ではありません。
常に 100% の稼働率を達成できないのはなぜですか? SM には、レジスタ、共有メモリ、スレッド ブロック スロット、スレッド スロットなどの実行リソースの固定セットがあります。これらのリソースは、需要と GPU の制限に基づいてスレッド間で動的に分割されます。たとえば、NVIDIA H100 では、各 SM は 32 のスレッド ブロック、64 のワープ (つまり 2048 スレッド) を処理でき、各スレッド ブロックには 1024 のスレッドがあります。1024 スレッドでグリッドを開始すると、GPU は 2048 個の利用可能なスレッド スロットを 2 つのスレッド ブロックに分割します。
動的パーティショニングと固定パーティショニング: 動的パーティショニングにより、GPU コンピューティング リソースをより効率的に使用できます。対照的に、固定パーティショニングでは各スレッド ブロックに固定量の実行リソースが割り当てられますが、これが常に最も効率的であるとは限りません。場合によっては、固定パーティショニングにより、スレッドに実際に必要なリソースよりも多くのリソースが割り当てられ、リソースが無駄になり、スループットが低下する可能性があります。
以下では、例を使用して、SM 占有に対するリソース割り当ての影響を説明します。32 スレッドのスレッド ブロックを使用し、合計 2048 スレッドが必要だと仮定すると、そのようなスレッド ブロックが 64 個必要になります。ただし、各 SM が一度に処理できるスレッド ブロックは 32 個のみです。したがって、SM は 2048 スレッドを実行できますが、一度に実行できるスレッドは 1024 個のみで、占有率は 50% にすぎません。
同様に、各 SM には 65536 個のレジスタがあります。2048 個のスレッドを同時に実行するには、各スレッドに最大 32 個のレジスタがあります (65536/2048 = 32)。カーネルがスレッドごとに 64 個のレジスタを必要とする場合、各 SM は 1024 個のスレッドしか実行できず、占有率も 50% になります。
占有不足の課題は、ハードウェアから最適なパフォーマンスを達成するために十分な遅延耐性や必要な計算スループットを提供できない可能性があることです。
GPU カーネルを効率的に作成するのは複雑な作業です。高い占有率を維持しながら、リソースを合理的に割り当て、待ち時間を最小限に抑える必要があります。たとえば、レジスタの数が多いとコードの実行が速くなりますが、占有率が低下する可能性があるため、コードを慎重に最適化することが重要です。
6
要約する
新しい用語や概念が膨大にあるため、読者は気が遠くなるかもしれないことは理解しています。そのため、簡単な復習のために最後に重要なポイントをまとめています。
-
GPU は複数の SM で構成され、各 SM には複数の処理コアが含まれます。
-
GPU にはオフチップ グローバル メモリがあり、通常は高帯域幅メモリ (HBM) またはダイナミック ランダム アクセス メモリ (DRAM) です。チップ上の SM から遠く離れているため、レイテンシは高くなります。
-
GPU には、オフチップ L2 キャッシュとオンチップ L1 キャッシュの 2 つのレベルのキャッシュがあります。L1 キャッシュと L2 キャッシュは、CPU の L1/L2 キャッシュと同様に機能します。
-
各 SM には、構成可能な共有メモリの小さな部分があります。この共有メモリは処理コア間で共有されます。通常、スレッド ブロック内のスレッドは、毎回グローバル メモリからデータをロードするのではなく、データの一部を共有メモリにロードし、必要に応じて再利用します。
-
各 SM には多数のレジスタがあり、レジスタはスレッドの要件に応じて分割されます。NVIDIA H100 には、SM ごとに 65536 個のレジスタがあります。
-
GPU でカーネルを実行するときは、スレッド グリッドを開始する必要があります。グリッドは 1 つ以上のスレッド ブロックで構成され、各スレッド ブロックは 1 つ以上のスレッドで構成されます。
-
リソースの可用性に応じて、GPU は SM 上で実行する 1 つ以上のスレッド ブロックを割り当てます。同じスレッド ブロック内のすべてのスレッドは、実行のために同じ SM に割り当てられます。この目的は、データの局所性を最大限に活用し、スレッド間の同期を実現することです。
-
SM に割り当てられたスレッドは、ワープと呼ばれるサイズ 32 のグループにさらに分割されます。ワープ内のすべてのスレッドは同じ命令を同時に実行しますが、データの異なる部分 (SIMT) で実行されます (ただし、新しい世代の GPU は独立したスレッド スケジューリングもサポートしています)。
-
GPU は、各スレッドのニーズと SM の制限に基づいて、スレッド間でリソースを動的に分割します。プログラマーは、実行中の SM 占有率を最大限に高めるためにコードを慎重に最適化する必要があります。
脚注
[1] はい、ハイパー スレッディング テクノロジとマルチコア プロセッサのおかげで、CPU はタスクを並行して実行することもできます。しかし、長い間、逐次実行のパフォーマンスを向上させるために多くの努力が費やされてきました。
[2] 現世代の NVIDIA GPU では、ワープ サイズは 32 です。ただし、このサイズは将来のハードウェアの反復で変更される可能性があります。
他のみんなも見てるよ
OneFlow を試してください: github.com/Oneflow-Inc/oneflow/
この記事は、WeChat パブリック アカウント - OneFlow (OneFlowTechnology) から共有されています。
侵害がある場合は、削除について [email protected] までご連絡ください。
この記事は「OSC ソース作成計画」に参加していますので、読んでいる方もぜひ参加し、共有してください。