AIシステムの視点からCUDAを考察する

AIシステムの視点からNVIDIAのエコシステムを再評価すると、多くの参考になる側面が見えてきます。本稿では主にパイプラインスケジューリング、SIMTフロントエンド、分岐予測、およびインタラクション方式について分析し、DSAアーキテクチャと比較しながら、NVIDIA CUDAから学べる点について考察します。

NVIDIAエコシステムの考察ポイント

ソフトウェアとハードウェアアーキテクチャの観点から、CUDAとSIMTの間には一定の関係性があり、現在AIチップで採用されているDSAアーキテクチャはプログラミングモデルとハードウェア実行モデルの両面でまだ比較的初期の段階にあります。NVIDIAの強力なエコシステムも、CUDAが提供するプログラミングにおける使いやすさに依存しています。

新しいAIチップに対して、パイプライン隠蔽の観点では、アーキテクチャレベルでの隠蔽パイプラインスケジューリングメカニズムを実現し、SPMDと形式上関連のないプログラミングモデルを提案し、しかもCUDAと同等の使いやすさを持つソフトウェアは可能です。しかし逆に、核心的な問題が解決されていない場合、CUDAと形式上類似したプログラミングモデルを提案しても使いやすさの問題が残り、開発者が十分に良い初期パフォーマンスを得ることは困難です。

ソフトウェアとハードウェアのアーキテクチャにおいて、DSAアーキテクチャは一方でオープンなソフトウェアとハードウェアのアーキテクチャを確立し、他のDSAアーキテクチャと連携してCUDAエコシステムに対抗する必要があります。他方で、異なるレベルの開発者向けに使いやすさとソフトウェア開発の形態を明確にする必要があります。

SIMTとCUDAの関係

NVIDIAはCUDAエコシステムを維持するためにSIMTハードウェアアーキテクチャに対して調整と妥協を行ったため、CUDAはある程度NVIDIAのハードウェアアーキテクチャに制約を与えます。例えば、SM、Warp、Threadなどのスレッド階層概念を維持しています。CUDAアーキテクチャは近年大きな変更はなく、主に外部に対する抽象化と使いやすさを維持するためのプログラミングシステムとして機能しています。

DSAがハードウェアアーキテクチャの命令と設計において比較的革新的である理由は、ソフトウェアシステムが優れているからではなく、初期段階でプログラミングシステムの問題をあまり考慮していなかったためです。その結果、ハードウェアとソフトウェアの協調によるアーキテクチャ制約を実現するための妥協もありませんでした。CUDAの成功は、SIMTアーキテクチャを通じてパイプラインスケジューリング、並列命令隠蔽、およびCUDAの使いやすさを隠蔽した点にあります。

DSAハードウェアアーキテクチャの実行方式

DSAハードウェアアーキテクチャは一般的に単一コア単一スレッドを指し、スレッド内の命令は複数のコア間で共有キャッシュを介して協調できます。プログラミングモデルには統一された標準が欠けているため、専用のコンパイラとプログラミングシステムを構築する必要があります。ハードウェアは主にAIアクセラレータチップ(TPU、NPUなど)が中心です。

DSAのハードウェア実行方式について、DSAハードウェアの現在の裸のインターフェースは通常各コアに1つのスレッドがあり、各スレッド内でDSA命令セットを順次呼び出します。命令はハードウェア上で通常異なる命令実行パイプラインに分散され、正確性の一部はソフトウェア同期によって保証され、一部はハードウェアによって保証されます。

CUDAユーザーの能力区分

CUDAの使用の難易度に基づいて、CUDAのユーザーを3つのカテゴリに分類できます:初級、中級、上級ユーザーです。

  • 初級ユーザー:CUDAの並列プログラミング能力を習得し、NV SIMTハードウェアの基本アーキテクチャを理解し、並列命令、パイプライン隠蔽、並列計算の3つのパフォーマンスを得ることができます。
  • 中級ユーザー:CUDAが提供するブロック化Tiling、パイプラインPipeline機能をさらに活用し、より高いパフォーマンス収益を得ます。
  • 上級ユーザー:SIMTマイクロアーキテクチャの詳細を深く理解し、スレッドバンク競合、精緻なパイプライン隠蔽、精緻な命令使用、極致のブロック化Tiling戦略を解決し、極致のパフォーマンスを実現します。

CUDAは開発において優れた使いやすさを持ちます。以下はCPUを使用して記述された行列加算演算です:

void matrix_add(float* a, float* b, float* c, int size) {
    int idx;

    for (int i = 0; i < size; ++i) {
        idx = i * size + j;
        c[idx] = a[idx] + b[idx];
    }
}

int main()
{
    matrix_add(a, b, c, size);
}

以下はGPUを使用して記述された行列加算演算です。CPUプログラミングと比較して、並列計算を使用しているためforループがありません:

__global__ void matrix_add(float* a, float* b, float* c, int size) {
    // グローバルインデックスを計算
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    int j = blockIdx.y * blockDim.y + threadIdx.y;
    int idx = i * size + j;

    // インデックスが範囲内かチェック
    if (i < size && j < size) {
        // 行列加算を実行
        c[idx] = a[idx] + b[idx];
    }
}

int main() {
    dim3 block_dim(block_size, block_size);

    // グリッドサイズを計算
    dim3 grid_dim((size + block_dim.x - 1) / block_dim.x, (size + block_dim.y - 1) / block_dim.y);

    // カーネルを起動
    matrix_add<<<grid_dim, block_dim>>>(a, b, c, size);
}

優れたハードウェアアーキテクチャとソフトウェアエコシステムを組み合わせることで、NVIDIA GPUとCUDAはSIMTの最も成功した実践例です。

参考と考察ポイント

パイプラインスケジューリング

命令パイプラインスケジューリングにおいて、最も重要なのはハードウェア設計からSIMDデータパスのパイプラインスケジューリング問題を解決することです。プログラム実行の最大のボトルネックはメモリアクセスと制御フローであり、単一スレッドのCPUは分岐予測、アウトオブオーダー実行、キャッシュ、プリフェッチなどのメカニズムを大量に使用してメモリアクセスと制御フローが直面するボトルネックを緩和する必要があります。SIMDは通常CPU自身のアウトオブオーダー、投機的実行、キャッシュ、プリフェッチ能力に依存してこれらの問題を緩和します。NVIDIA GPUは多スレッドのインターリーブ実行によって全体の並列計算パフォーマンスを向上させます。多数のスレッドが異なるブロックとスレッドを通じてデータを読み込み、計算命令を実行します。

DSA上でSIMDハードウェアにSIMTフロントエンドをカプセル化しても、実行命令に依存関係がある場合、基礎的なパフォーマンスは非常に悪くなります。パイプラインスケジューリングは依然として開発者が手動で行う必要があり、開梱してすぐに使える、パフォーマンスの良いコードを書くことは困難です。

SIMTフロントエンドハードウェア

SIMTフロントエンドハードウェアを追加することで、スレッドグループWarpを通じてスレッド命令パイプラインを隠蔽します。CUDAプログラミングモデルでは、各スレッドブロック(thread block)内部に多くの並列スレッドが必要であり、暗黙的にいくつかのWarpに分割され、各Warpは順次インターリーブされたメモリアクセスと計算を含みます。GPUはWarp Schedulerを通じて動的にインターリーブ実行します。Warp0のパイプラインがブロックされると、次のWarp1に切り替えられます。Warpの並列性を通じて命令パイプラインブロックを暗黙的に隠蔽し、開発者は良いパフォーマンスを得られます。

DSAハードウェアアーキテクチャもWarp Schedulerを導入して命令パイプラインブロックを隠蔽し、各DSAコアが複数のスレッドを実行し、相互にパイプラインブロックを隠蔽できます。NVIDIA GPUがWarpを使用して命令パイプラインを隠蔽するのは実行時の具体的な情報に基づいていますが、開発者とコンパイラは静的情報に基づいてパイプラインスケジューリングを行うため、十分にバランスの取れたスケジューリングは難しく、SIMD/DSAで手動またはコンパイラによる自動パイプラインスケジューリングを行う際に相対的に困難です。ベテラン開発者でもパイプラインスケジューリングを十分に良くすることは困難です。

SIMTフロントエンドハードウェアの追加はオーバーヘッドも带来しますが、パイプラインブロック隠蔽を実現できます。SIMT表現を通じてインターフェースをユーザーに公開し、ユーザーが能動的にマルチスレッドを書き、Warp Schedulerがハードウェアレベルでマルチスレッド間のパイプラインブロックを相互に隠蔽します。SIMD命令隠蔽はSIMT表現を通じてユーザーが汎用の単一スレッドを書き、同時にWarpグループがSIMD命令を構成することで実現できます。

しかしCUDAはDSA命令隠蔽を解決していません。現在は開発者にWarp概念を与え、命令APIを透過的に伝えることで表現と使用の問題を解決しています。そのためCUDAの学習コストは低くなく、事前にNVIDIA GPUのハードウェア詳細を十分に理解する必要があります。

分岐予測メカニズム

SPMDプログラミングモデルの分岐予測と制御フローに対する高い許容度は、使いやすさを支える重要な手段です。分岐と連続メモリアクセスの削減はソフトウェアレベル、使いやすさの観点で注目すべき最適化ポイントです。もちろん、SIMDハードウェア上でもPredicate/mask、gather/scatter命令やmemory coalescingを通じて実現できます。コンパイラによる分岐予測を実現し、開発者が無感でパフォーマンスを向上させることができますが、SIMDスレッド数が限られている場合、パフォーマンスの向上は難しい問題になるかもしれません。

NVIDIA GPUはWarpベースのSIMD上でスレッドが異なる分岐を実行できるようにし、各スレッドは条件付き制御フロー命令(Conditional Control Flow Instructions)を実行できます。同時に異なるスレッド間でそれぞれ異なる制御フローパス(Different Control Flow Paths)を実行できます。例えば、Thread W、Thread X、Thread Yの制御フローパスをそれぞれ実行します。

しかしSIMTの制御フローには多くの問題があります。そのためCUDAプログラミングで大量のif/else文が出現することは推奨されません。通常、SIMDパイプラインを使用して制御ロジックの面積を節約します。例えば、ScalarスレッドをWarps内に配置します。Warp内のスレッドが異なる実行パスに分岐すると、分岐実行の競合が発生します。例えば、Path 1とPath 2の2つの分岐パスがある場合、異なる時間に異なるパスを実行できますが、これにより時間コストが増加します。

分岐予測の問題を解決するために、動的Warp Formating/Mergingは分岐後に同じ命令を実行するスレッドを動的に結合し、待機中のWarpsから新しいWarpsを形成します。分岐下の各パスのスレッドは新しいWarpを作成するために使用されます。Warp XとWarp YをWarp Zに結合し、同じ命令をより良く実行できます。

Path 1とPath 2の2つのパスがある場合、一部のクロックサイクルが空であるため、動的結合分岐後に同じ命令を実行するスレッドを形成し、異なるコードパスを同時に実行してスレッド間の待機とリソースの無駄を避けます。

動的Warpグループ化(Dynamic Warp Formation)は主にコンパイラレベルで分岐予測の問題を解決し、スレッドの実行状況とデータ依存性に基づいてWarp内のスレッドを動的に組織し、並列計算パフォーマンスとリソース利用率を向上させ、GPU計算を最適化し、プログラムの実行効率を向上させます。

インタラクション方式

CUDAはhost(CPU)とdevice(GPU)間の便利なインタラクション方式を提供できます。CUDAにはSIMT、SIMD、DSAのハードウェアアーキテクチャ自体と直接関係のない多くの実装メカニズムがあります。CUDAのすべての特性もSIMTアーキテクチャに固有のものではなく、したがって技術的にSIMT、SIMD、DSAとハードウェアを強制的にバインドする必要はありません。例えば、CUDAランタイムはhostとdevice間のC++インタラクション方式を提供し、Cambricon BANG C言語はこのレベルでCUDAを参考にしています。ソフトウェアレベルのインタラクションにおいて、CUDAは簡単にベクトル加算を実装できます:

for (int i = 0; i < 10000; ++i) {
    C[i] = A[i] + B[i];
}
#include <stdio.h>

// ベクトルサイズを定義
#define SIZE 5

// CUDAカーネル関数、ベクトル加算を実行
__global__ void vector_add(int *a, int *b, int *c) {
    int i = blockDim.x * threadDim.x + threadIdx.x;

    if (i < SIZE) {
        c[i] = a[i] + b[i];
    }
}

int main() {
    int a[SIZE], b[SIZE], c[SIZE];
    int *d_a, *d_b, *d_c;

    // デバイス上にメモリを割り当て
    cudaMalloc((void**)&d_a, SIZE * sizeof(int));
    cudaMalloc((void**)&d_b, SIZE * sizeof(int));
    cudaMalloc((void**)&d_c, SIZE * sizeof(int));

    // ベクトルaとbを初期化
    for (int i = 0; i < SIZE; i++) {
        a[i] = i;
        b[i] = SIZE - i;
    }

    // ベクトルaとbをデバイスにコピー
    cudaMemcpy(d_a, a, SIZE * sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, b, SIZE * sizeof(int), cudaMemcpyHostToDevice);

    // CUDAカーネル関数を呼び出し
    vector_add<<<1, SIZE>>>(d_a, d_b, d_c);

    // 結果をホストにデバイスからコピー
    cudaMemcpy(c, d_c, SIZE * sizeof(int), cudaMemcpyDeviceToHost);

    // 結果を表示
    for (int i = 0; i < SIZE; i++) {
        printf("%d ", c[i]);
    }
    printf("\n");

    // デバイス上のメモリを解放
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);

    return 0;
}

CUDAはプログラミング開発において優れた使いやすさを持ちます。初級ユーザーにとって、CUDAの使いやすさは極めて高く、入門開発者が単純なオペレータ(Kernel)を任意に書くだけで、CPUよりも5~10倍のピークパフォーマンスを得られます。

DSAハードウェアアーキテクチャはパイプラインと命令使用において完全で暗黙的なサポートが欠けているため、命令パイプラインのサポートは開発者が手動で隠蔽、ブロック化などの他の最適化思想を通じてこの部分のパフォーマンスを補う必要があります。低レベル命令を使用すると、ユーザーは正しいKernelを書くためにより多くの時間を費やすことになります。

タグ: CUDA SIMT DSA GPU AIチップ

6月6日 17:16 投稿