CUDA10.0公式文書の翻訳と学習のためのCUDA協力グループ

目次

バックグラウンド

前書き

ブロック内グループ

スレッドグループとスレッドブロック

タイル張りのパーティション

スレッドブロックタイル

疑似スレッドシャッフル機能

疑似スレッド投票機能

疑似スレッドマッチング機能

グループをマージ

ブロック内での協力グループの使用

ディスカバリーテンプレート

疑似スレッド同期コードテンプレート

組み合わせ

グリッド同期

マルチデバイス同期

結論

バックグラウンド

今日、私たちは注目に値するCUDA10.0公式文書の最後の部分である協力グループを翻訳します

前書き

協調グループは、通信スレッドグループを編成するためにCUDA9で導入されたCUDAプログラミングモデルの拡張です。協力グループにより、開発者はスレッド通信の粒度を表現して、より豊かで効果的な並列分解を表現できます。

これに先立ち(記事CUDAプログラミングモデルを参照)、CUDAプログラミングモデルは、協調スレッドを同期するための単一の単純な構造を提供していました。それは、__ syncthreads()関数で実装されたスレッドブロック内のすべてのスレッドにわたるバリアです。ただし、プログラマーは、他の詳細なスレッドグループの同期を定義および同期して、パフォーマンスの向上、設計の柔軟性、およびグループレベルの関連関数インターフェイスのソフトウェア再利用をサポートしたいと考えています。より広範な並列対話テンプレートを表現するために、多くのパフォーマンス指向のプログラマーは、疑似スレッド内のスレッドまたは単一のGPU上の異なるスレッドブロックの同期のために、個別にカスタマイズされたが安全ではない独自の関数を実装しました。達成されたパフォーマンスの向上は目覚ましいものですが、これにより断片化されたコードが急増し、時間の経過やGPUの更新の実装、調整、維持が困難になっています。協力チームは、優れたパフォーマンスでコードをサポートするための安全で将来性のあるメカニズムを提供することにより、この問題を解決します。

Cooperative Group Programming Model拡張機能は、CUDAスレッドブロックとCUDAスレッドブロック間の同期テンプレートを記述します。これにより、アプリケーションは独自のスレッドグループと同期スレッドグループを定義できます。また、特定の制限を適用する新しい方法も提供されます。同期が機能することを保証するAPI。これらの関数は、生産者/消費者並列処理、日和見並列処理、グリッド全体でのグローバル同期など、CUDAの協調並列処理の新しいテンプレートを利用します。

グループを一次プログラムオブジェクトとして表現すると、関連する関数がスレッドグループへの参加を表す明確なオブジェクトを受け取ることができるため、ソフトウェアの構成が改善されます。このオブジェクトにより、プログラマーの意図を明確にする、つまり排除することもできます。断片化されたコードと不合理なコンパイラの最適化。不健全なアーキテクチャの仮定を制限し、新しいGPUバージョンによりよく適応します。

協調グループプログラミングモデルは、次の要素で構成されています。

  • 協調スレッドのデータ型を表します。
  • CUDAスタートアップAPIによって定義された命令レベルグループの操作を取得します。
  • 既存のグループを新しいグループに分割する操作。
  • 特定のグループのフェンス操作を同期します。
  • 特定のグループコレクションのグループ属性と操作を表示する

ブロック内グループ

このセクションでは、ブロック内で同期および協調できるスレッドグループを作成するために使用できる関数について説明します。スレッドブロックまたはデバイス間で協調グループを同期するには、いくつかの追加の考慮事項が必要です。これについては後で説明します。

協調グループにはCUDAバージョン> = 9.0が必要です。この関数を使用するには、ヘッダーファイル#include <cooperative_groups.h>を追加し、協調グループ名前空間を使用する必要があります:名前空間cooperative_groups;を使用してから、コードを含めます任意のブロックの協調グループ関数の例nvccを使用して、通常の方法でコンパイルできます。

スレッドグループとスレッドブロック

CUDAプログラマーは、スレッドのセット(スレッドブロック)に既に精通している必要があります(CUDAプログラミングモデルの記事を参照)。協調グループの拡張により、カーネル関数でこの概念を明確に表現するための新しいデータ型-thread_blockが導入されます。グループは次のように初期化できます。thread_blockg= this_thread_block();。thread_blockデータ型はより一般的なthread_groupデータから取得されます。タイプ。thread_groupは、より広範囲のグループを表すために使用でき、次の関数を提供します。

void sync(); // 同步组内线程
unsigned size(); // 组内线程数
unsigned thread_rank(); // 调用线程的组内序号,值域为[0, size]
bool is_valid(); // 组是否违反了任何API约束

また、thread_blockは、次の追加のブロックベースの関数を提供します。

dim3 group_index(); // 网格内的块索引,三维
dim3 thread_index(); // 块内的线程索引,三维

たとえば、グループgが上記のように初期化されている場合、g.sync();はブロック内のすべてのスレッドを同期します。これは__syncthreads();と同等です。グループ内のすべてのスレッドは均一な操作を実行する必要があることに注意してください。そうしないと、未定義の動作が発生します。

タイル張りのパーティション

tile_partition()関数を使用して、スレッドブロックを複数の小さな協調スレッドグループに分解できます。たとえば、最初にブロック内のすべてのスレッドを含むグループを作成すると、次のようになります。

thread_block wholeBlock = this_thread_block();

次に、グループごとに32スレッドなど、より小さなグループに分割できます。

thread_group tile32 = tiled_partition(wholeBlock, 32);

さらに、32スレッドの各グループを、グループごとに4スレッドなど、より小さなグループに分割できます。

thread_group tile4 = tiled_partition(tile32, 4);

次に、次のコードを追加すると、次のようになります。

if (tile4.thread_rank() == 0) printf(“Hello from tile4 rank 0\n”);

次に、段落が4つのスレッドごとに印刷されます。各tile4グループの0番目のスレッドと、wholeBlockグループの0番目、4番目、8番目、および12番目のスレッドが出力されます。現在のスライスサイズは2の累乗であり、32を超えることはできないことに注意してください。

スレッドブロックタイル

tiled_pa​​rtition関数のテンプレートバージョンを使用することもできます。この場合、テンプレートパラメーターを使用してスライスのサイズを指定します。これはコンパイル時に決定されるため、実行を最適化する余地があります。前のセクションと同様に、次のコードはサイズ32と4の2セットのスライスを作成します。

thread_block_tile<32> tile32 = tiled_partition<32>(this_thread_block());
thread_block_tile<4> tile4 = tiled_partition<4>(this_thread_block());

ここではthread_block_tileテンプレートデータ構造が使用されており、グループサイズは関数パラメーターではなくテンプレートパラメーターとしてtiled_pa​​rtition()関数に渡されることに注意してください。

スレッドブロックシャーディングは、次の追加機能も提供します。

.shfl()
.shfl_down()
.shfl_up()
.shfl_xor()
.any()
.all()
.ballot()
.match_any()
.match_all()

これらの協調同期操作は、疑似スレッドシャッフル関数、疑似スレッド投票関数、および疑似スレッドマッチング関数に似ています。簡単な紹介です。

疑似スレッドシャッフル機能

疑似スレッドシャッフル関数は、共有メモリを使用せずに疑似スレッド内のスレッド間でデータをブロードキャストするために使用されます。関数プロトタイプは次のとおりです。

T __shfl_sync(unsigned mask, T var, int srcLane, int width=warpSize);
T __shfl_up_sync(unsigned mask, T var, unsigned int delta, int width=warpSize);
T __shfl_down_sync(unsigned mask, T var, unsigned int delta, int
width=warpSize);
T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width=warpSize);

ここで、Tはブロードキャストされるデータ型であり、int、unsigned int、long、unsigned long、long long、unsigned long long、float、またはdoubleです。ヘッダーファイルcuda_fp16.hが含まれている場合、Tは__halfまたは__half2;マスクは交換を実行するターゲットスレッドをマークするために使用されます; srcLaneはブロードキャストを送信するソーススレッドを表します。ソーススレッドIDがwidthより大きい場合、実際のソーススレッドIDはsrcLane%widthに等しくなります; widthはブロードキャストを実行するためのパケットサイズ。2でなければなりません。整数で32を超えない場合、値は指定されたサイズグループでブロードキャストされます。関数は、ソースの値で指定された4バイトのワードを返します。糸;

__shfl_sync()関数のソーススレッドIDはsrcLane、__ shfl_up_sync()関数のソーススレッドIDはsrcLane-delta、__ shfl_down_sync()関数のソーススレッドIDはsrcLane + delta、__ shfl_xor_syncのソーススレッドIDは()関数はsrcLane xorlaneMaskです

疑似スレッド投票機能

疑似スレッド投票関数を使用すると、疑似スレッド内のスレッドでリダクションブロードキャスト操作を実行できます。これらの関数のプロトタイプは次のとおりです。

int __all_sync(unsigned mask, int predicate);
int __any_sync(unsigned mask, int predicate);
unsigned __ballot_sync(unsigned mask, int predicate);
unsigned __activemask();

述語は判断述語を表し、マスクは投票に参加しているスレッドを表します。この関数は、疑似スレッド内の各スレッドから整数述語を読み取り、これらの述語の値が0であるかどうかを読み取り、戻り値を参加している各スレッドにブロードキャストします。関数の実行ロジックを次の表に示します。

関数

実行ロジック

__all_sync()

マスクによって指定された、終了していないすべてのスレッドの述部値を評価し、すべてのスレッドの述部値がゼロ以外の場合にのみゼロ以外の値を返します

__any_sync()

マスクによって指定された、終了していないすべてのスレッドの述語値を評価します。いずれかのスレッドの述語値がゼロ以外の場合は、ゼロ以外の値を返します。

__ballot_sync()

マスクによって指定された、終了していないすべてのスレッドの述語値を評価し、整数を返します。疑似スレッドのN番目のスレッドがアクティブで、述部の値が0でない場合にのみ、整数のN番目のビットは1になります。

__activemask()

疑似スレッド内の現在アクティブなすべてのスレッドの4バイトマスクを返します。この関数が呼び出されたときに疑似スレッドのN番目のスレッドがアクティブである場合、マスクのN番目のビットは1であり、終了したスレッドまたは非アクティブなスレッドに対応するコードビットは0です。この関数が呼び出されたときに収束するスレッドは、これらの命令が疑似スレッドの組み込み同期関数でない限り、ダウンストリーム命令で収束することを保証できないことに注意してください。

疑似スレッドマッチング機能

疑似スレッドマッチング関数は、疑似スレッド内のスレッド間で同期ブロードキャスト比較操作を実行します。計算能力が7.X以上のデバイスをサポートします。関数プロトタイプは次のとおりです。

unsigned int __match_any_sync(unsigned mask, T value);
unsigned int __match_all_sync(unsigned mask, T value, int *pred);

Tは、int、unsigned int、long、unsigned long、long long、unsigned long long、float、またはdoubleのいずれかです。値はブロードキャストによって比較される値を示し、マスクは参加するスレッドを指定します。次の表に示すように、これら2つの関数の戻りロジックは異なります。

関数

リターンロジック

__match_any_sync()

値がvalueと等しいmaskで指定されたスレッドのスレッドマスクを返します

__match_all_sync()

maskで指定されたすべてのスレッドの値がvalueと同じである場合、maskが返され、predがtrueになります。それ以外の場合、0が返され、predはfalseになります。

協調グループスレッドのスレッドブロックスライスセクションに戻ると、これらの関数はユーザー定義のスレッドグループのコンテキストで使用され、柔軟性と生産性が向上します。

グループをマージ

CUDAのSIMTアーキテクチャ(CUDAハードウェア実装の記事を参照)では、ハードウェアレベルで、マルチプロセッサは32スレッドをグループ(疑似スレッド)として使用してスレッドを実行します。アプリケーションコードにデータ依存の条件付き分岐がある場合、疑似スレッドのスレッドは分散し、疑似スレッドは各分岐をウォークスルーし、そのパス上にないが現在のスレッドでアクティブなスレッドをブロックします実行パススレッドの実行は、複合実行と呼ばれます。協調グループには、マージされたすべてのスレッドグループを検出または作成する機能があります。coalesced_groupactive= coalesced_threads();。たとえば、シナリオを考えてみましょう。コードには、各疑似スレッドの2番目、4番目、8番目のスレッドのみがアクティブなままであるブランチがあります。このブランチで実行されるステートメントは、各疑似スレッドの名前を作成します。アクティブグループには次のものが含まれます。 3つのアクティブなスレッド(グループ内のIDはそれぞれ0、1、2です)

ブロック内での協力グループの使用

このセクションでは、協同組合グループの機能をいくつかの例で説明します

ディスカバリーテンプレート

通常の状況では、開発者はアクティブなスレッドセットを操作する必要があります。現在使用可能なスレッドを想定または指定することはできませんが、たまたまそこにあるスレッドのみを操作できます。「スレッド内のクロススレッドの集約アトミック加算」を参照してください。 "(正しいCUDA 9.0関数を使用して記述):

{
    unsigned int writemask = __activemask();
    unsigned int total = __popc(writemask); // 活跃的线程数
    unsigned int prefix = __popc(writemask & __lanemask_lt()); // 当前活跃线程前缀,比如活跃线程掩码为01010,那么对于第2个线程,__lanemask_lt()为00001,那么prefix就是0(第4个活跃线程对应的就是1)。因此前缀为0就表示当前为第一个活跃的线程

    int elected_lane = __ffs(writemask) - 1; // id最小的活跃线程
    int base_offset = 0;
    if (prefix == 0) {
        base_offset = atomicAdd(p, total);
    }

    base_offset = __shfl_sync(writemask, base_offset, elected_lane); // 把elected_lane中原子加前的值广播到所有的活跃线程中
    int thread_offset = prefix + base_offset;

    return thread_offset;
}

協調グループAPIを使用して書き直すと、次のコードが得られます。

{
    cg::coalesced_group g = cg::coalesced_threads(); // 活跃线程组

    int prev;
    if (g.thread_rank() == 0) { // 第一个活跃线程
        prev = atomicAdd(p, g.size()); // 原子加
    }

    prev = g.thread_rank() + g.shfl(prev, 0); // 最小的活跃线程id + 老的值
    return prev;
}

疑似スレッド同期コードテンプレート

開発者は、疑似スレッド同期コードを持っていて、疑似スレッドのサイズを暗黙的に想定し、このサイズに従ってコード化されている場合があります。ここで、疑似スレッドサイズを明示的に指定する必要があります。

auto g = tiled_partition<16>(this_thread_block());

ただし、ユーザーはアルゴリズムをより適切に分割し、組み込みのテンプレートパラメーターを同期するために疑似スレッドを使用したくない場合があります。

auto g = tiled_partition(this_thread_block(), 8);

この場合、グループgは引き続き同期でき、それに基づいて複数の並列アルゴリズムを構築できますが、shfl()などの関数は使用できません。

__global__ void cooperative_kernel(...) {
    // 获取默认的块线程组
    thread_group my_block = this_thread_block();

    // 分组成32个线程一组的线程组(片),线程片将线程组平均瓜分,每个片内的线程都是连续的
    thread_group my_tile = tiled_partition(my_block, 32);

    // 只在块内前32个线程中执行操作
    if (my_block.thread_rank() < 32) {
        // ...
        my_tile.sync();
    }
}

組み合わせ

以前は、コードを作成するときに、次のコードなど、実装にいくつかの暗黙の制限がありました。

__device__ int sum(int *x, int n) {
    // ...
    __syncthreads();
    return total;
}

__global__ void parallel_kernel(float *x){
    // ...
    // 所有的线程块都要调用sum()
    sum(x, n);
}

スレッドブロック内のスレッドは__syncthreads()バリアに到達する必要がありますが、この制限は、sum()を呼び出す開発者には見えません。次に、協力グループを使用して、これを達成するためのより良い方法は次のとおりです。

__device__ int sum(const thread_group& g, int *x, int n)
{
    // ...
    g.sync()
    return total;
}

__global__ void parallel_kernel(...)
{
    // ...
    sum(this_thread_block(), x, n);
    // ...
}

グリッド同期

協調グループ同期が導入される前は、CUDAプログラミングモデルはカーネル関数が完了したときにのみスレッドブロック間の同期を許可し、カーネル関数の境界には暗黙の無効な状態と潜在的なパフォーマンスへの影響がありました。たとえば、特定のユースケースでは、アプリケーションに多数の小さなカーネル関数があり、各カーネル関数はパイプラインのステージを表します。現在のCUDAプログラミングモデルでは、パイプラインの下位ステージのスレッドブロックがデータを消費する準備ができる前に、これらのカーネル関数がデータを生成する必要があります。この場合、グローバルスレッドブロック間の同期を提供する機能により、アプリケーションはこれらのスレッドブロックを再構築して、特定のフェーズが完了したときにデバイスを同期できます。

カーネル関数内でグリッドを同期するには、group:grid_group grid = this_grid();を使用してから、grid.sync();を呼び出すことができます。セル同期をサポートするには、<<< >>>実行構成構文の代わりに、カーネル関数を開始するときにCUDAランタイム起動APIであるcudaLaunchCooperativeKernel()を使用する必要があります。

cudaLaunchCooperativeKernel(const T *func, dim3 gridDim, dim3 blockDim, void **args, size_t sharedMem = 0, cudaStream_t stream = 0)
// 或者CUDA驱动API的对应函数,这种核函数不能使用附录A中的动态并行功能

GPU上でスレッドブロックが共存するようにするには、開始されたブロックの数を慎重に検討する必要があります。たとえば、次のように開始できます。

cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
// 初始化,而后启动
cudaLaunchCooperativeKernel((void*)my_kernel, deviceProp.multiProcessorCount, numThreads, args);

または、次の方法で占有計算機を使用して、マルチプロセッサ上に同時に存在できるスレッドブロックの数を計算できます。

cudaOccupancyMaxActiveBlocksPerMultiprocessor(&numBlocksPerSm, my_kernel, numThreads, 0));
// 初始化,而后启动
cudaLaunchCooperativeKernel((void*)my_kernel, numBlocksPerSm, numThreads, args);

また、グリッド同期を使用するには、デバイスコードを個別にコンパイルしてから、デバイスの実行時にリンクする必要があることに注意してください(詳細については、独立したコンパイルの章を使用したCUDAコンパイラドライバーNVCCドキュメントを参照してください)。最も簡単な例は次のとおりです。以下に示す:

nvcc -arch=sm_61 -rdc=true mytestfile.cu -o mytest

また、デバイスが協調的なスタートアップ属性をサポートしていることを確認する必要があります。これは、CUDAドライバーAPI cuDeviceAttribute()を使用して表示できます。

int pi=0;
cuDevice dev;
cuDeviceGet(&dev,0) // 查询设备0
cuDeviceGetAttribute(&pi, CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH, dev);

piが1の場合、この属性はデバイス0でサポートされており、コンピューティング機能が6.0以上のデバイスのみが協調スタートアップ属性をサポートできることを意味します。さらに、MPSのないLinuxプラットフォームまたはTCCモードの機器を備えたWindowsプラットフォームで、協調起動機能を使用してプログラムを実行する必要があります。

マルチデバイス同期

協調グループを使用した複数のデバイス間の同期をサポートするには、CUDA api cuLaunchCooperativeKernelMultiDevice()を使用する必要があります。これは、既存のCUDA APIの重要な拡張であり、ホストスレッドをサポートして複数のデバイスでカーネル関数を開始します。cuLaunchCooperativeKernel()関数以外の制限と保証に加えて、cuLaunchCooperativeKernelMultiDevice()関数には次のセマンティクスがあります。

  • このAPIは、起動がアトミックであることを保証します。つまり、APIが正常に呼び出されると、指定された数のスレッドブロックが指定されたすべてのデバイスで開始されます。
  • このAPIを介して起動されるカーネル関数は同じである必要があります。このチェックは基本的にドライバーでは実行できないため、ドライバーのこの部分は明示的なチェックを行いません。したがって、アプリケーションはこれを確認する必要があります。
  • launchParamsListパラメーターの2つの要素を1つのデバイスにマップすることはできません。
  • この種のスタートアップのターゲットデバイスは、同じ計算能力を備えている必要があります。メジャーバージョンまたはマイナーバージョンは同じである必要があります。
  • スレッドブロックサイズ、グリッドサイズ、および各セルで使用される共有メモリの量は、すべてのデバイスで等しくなければなりません。これは、各デバイスによって開始されるスレッドブロックの最大数が、マルチプロセッサの数が最も少ないデバイスに依存することを意味することに注意してください。
  • 開始するcu関数を呼び出すモジュールに存在するカスタム__device __、__ constant__、または__managed__デバイスグローバル変数は、各デバイスで個別に初期化されます。ユーザーは、そのようなデバイスグローバル変数が正しく初期化されていることを確認する必要があります。

起動パラメータは、次の構造で定義する必要があります。

typedef struct CUDA_LAUNCH_PARAMS_st {
    CUfunction function;
    unsigned int gridDimX;
    unsigned int gridDimY;
    unsigned int gridDimZ;
    unsigned int blockDimX;
    unsigned int blockDimY;
    unsigned int blockDimZ;
    unsigned int sharedMemBytes;
    CUstream hStream;
    void **kernelParams;
} CUDA_LAUNCH_PARAMS;

次に、それをスタートアップAPIに渡します。

cudaLaunchCooperativeKernelMultiDevice(CUDA_LAUNCH_PARAMS *launchParamsList, unsigned int numDevices, unsigned int flags = 0);

この起動方法は、上記のグリッド同期の起動に似ています。また、同様の同期方法もあります。

multi_grid_group multi_grid = this_multi_grid();
multi_grid.sync();

また、独立したコンパイルを使用する必要があります。

前のセクションで説明したのと同じ方法で、デバイスがマルチデバイス起動属性をサポートしていることを確認する必要があります。パラメータをCU_DEVICE_ATTRIBUTE_COOPERATIVE_MULTI_DEVICE_LAUNCHに変更するだけで済みます。コンピューティング機能が6.0以上のデバイスのみが協調起動属性をサポートできます。さらに、MPSのないLinuxプラットフォームまたはTCCモードの機器を備えたWindowsプラットフォームで、協調起動機能を使用してプログラムを実行する必要があります。

結論

この時点で、CUDA10.0公式文書の翻訳の共有を終了しました。プロセス全体を個人的に翻訳しましたが、英語のレベルが制限されています。不適切な点がある場合は、コメント欄に提案してください。すみません。

おすすめ

転載: blog.csdn.net/qq_37475168/article/details/112388296