ストリーム
一般的に、cuda cの並列処理は、次の2つのレベルで表されます。
- カーネルレベル
- グリッドレベル
これまで、カーネルレベルについて説明してきました。つまり、カーネルまたはタスクは、GPU上の多くのスレッドによって並行して実行されます。ストリームの概念は後者に関連しています。グリッドレベルとは、デバイス上で複数のカーネルを同時に実行することを指します。
ストリームとイベントの概要
Cudaストリームとは、ホストコード呼び出しの順序でデバイス上で実行される一連の非同期cuda操作を指します。Streamはこれらの操作の順序を維持し、すべての前処理が完了した後にこれらの操作がワークキューに入るのを許可し、これらの操作に対していくつかのクエリ操作を実行することもできます。これらの操作には、ホストからデバイスへのデータ転送、カーネルの起動、およびデバイスによって実行されるその他のホスト開始アクションが含まれます。これらの操作の実行は常に非同期であり、cudaランタイムがこれらの操作の適切なタイミングを決定します。対応するcudaapiを使用して、すべての操作が完了した後に得られた結果が確実に得られるようにすることができます。同じストリーム内の操作には厳密な実行順序がありますが、異なるストリームにはこの制限はありません。
異なるストリームの操作は非同期で実行されるため、相互の調整を使用して、リソースの使用率を最大限に活用できます。私たちはすでに典型的なcudaプログラミングモデルに精通しています:
- ホストからデバイスに入力データを転送する
- デバイスでカーネルを実行します
- 結果をデバイスからホストに転送します
多くの場合、データを転送するよりもカーネルを実行する方がはるかに時間がかかるため、他のカーネルの実行でcpuとgpuの間の通信時間を隠すことは簡単に考えられます。これにデータ送信とカーネル実行を置くことができます。関数はさまざまなストリームに実装されています。ストリームを使用して、パイプラインおよびダブルバッファー(フロントバック)レンダリングを実装できます。
Cuda APIは、同期と非同期の2つのタイプに分けることができます。同期関数はホスト側のスレッドの実行をブロックし、非同期関数はすぐにホストに制御を戻し、後続のアクションを続行します。非同期関数とストリームは、グリッドレベルの並列処理の2つの基礎です。
ソフトウェアの観点からは、さまざまなストリームでさまざまな操作を並行して実行できますが、ハードウェアの観点からは必ずしもそうとは限りません。これは、PCIeリンクまたは各SMで使用可能なリソースによって異なります。異なるストリームは、他のストリームが実行を完了するまで待機する必要があります。以下では、さまざまなCCバージョンでのデバイス上のストリームの動作を簡単に紹介します。
Cuda Streams
すべてのcuda操作(カーネルの実行とデータ送信を含む)は、ストリーム内で明示的または暗黙的に実行されます。ストリームには、次の2つのタイプがあります。
- 暗黙的にストリームを宣言する(NULLストリーム)
- 宣言ストリームを表示する(NULL以外のストリーム)
デフォルトではNULLストリームであり、これまでストリームに関与したことがないブログ投稿ではこのタイプです。ストリームをNULL以外のストリームとして明示的に宣言した場合。
非同期およびストリームベースのカーネル実行とデータ送信により、次のタイプの並列処理を実現できます。
- ホスト操作とデバイス操作は並行しています
- ホストの算術演算とホストからデバイスへのデータ送信は並行しています
- ホストからデバイスへのデータ送信とデバイス操作の操作は並行しています
- デバイスでの並列操作
次のコードは以前の一般的な使用方法であり、デフォルトではNULLストリームが使用されます。
cudaMemcpy(...、cudaMemcpyHostToDevice); カーネル<<<グリッド、ブロック>>>(...); cudaMemcpy(...、cudaMemcpyDeviceToHost);
デバイスの観点からは、所有者の3つの操作はすべてデフォルトのストリームを使用し、コードの上から下に順番に実行されます。デバイス自体は、他のホスト操作がどのように実行されるかを認識していません。ホストの観点からは、データ送信は同期的であり、操作が完了するまで待機します。ただし、データ転送とは異なり、カーネルの起動は非同期であり、カーネルが実行されたかどうかに関係なく、ホストはほぼ即座に制御を取り戻し、次のステップに進むことができます。明らかに、この非同期動作は、デバイスとホスト間の計算時間をオーバーラップさせるのに役立ちます。
上記の内容は以前のブログ投稿で説明されています。ここでの特別な説明は、非同期で実行することもできるデータ送信です。これは、今回説明したストリームを使用します。実行をディスパッチするには、ストリームを明示的に宣言する必要があります。次のバージョンは、cudaMemcpyの非同期バージョンです。
cudaError_t cudaMemcpyAsync(void * dst、const void * src、size_t count、cudaMemcpyKind kind、cudaStream_t stream = 0);
新しく追加された最後のパラメータに注意してください。このように、ホストがデバイスを実行するためにこの機能を発行した後、制御権をすぐにホストに戻すことができます。上記のコードはデフォルトのストリームを使用しています。新しいストリームを宣言する場合は、次のAPIを使用してストリームを定義します。
cudaError_t cudaStreamCreate(cudaStream_t * pStream);
これは、cuda非同期API関数で使用できるストリームを定義します。この関数を使用する際の一般的なエラーの1つ、または混乱を引き起こしやすい場所は、この関数によって返されるエラーコードが、非同期関数が最後に呼び出されるまでに生成される可能性があることです。言い換えれば、エラーを返す関数は、エラーを生成するために関数を呼び出すための必要条件ではありません。
非同期データ転送を実行するときは、固定された(またはページングできない)メモリを使用する必要があります。固定メモリの割り当ては次のとおりです。詳細については、前のブログ投稿を参照してください。
cudaError_t cudaMallocHost(void ** ptr、size_t size); cudaError_t cudaHostAlloc(void ** pHost、size_t size、unsigned int flags);
メモリをホストの仮想メモリに固定することにより、メモリの物理的な場所をCPUメモリに強制的に割り当てて、プログラムライフサイクル全体を通じて変更されないようにすることができます。そうしないと、オペレーティングシステムは、ホスト側の仮想メモリに対応する物理アドレスをいつでも変更する可能性があります。非同期データ転送機能が固定されたホストメモリを使用しないと仮定すると、オペレーティングシステムはデータをある物理空間から別の物理空間に移動する可能性があります(非同期であるため、他のアクションを実行するCPUがこのデータに影響を与える可能性があります)。 cudaランタイムがデータ送信を実行している場合、これは未定義の動作につながります。
カーネルの実行時にストリームを設定する場合は、ストリームパラメータを追加する限り、非常に簡単です。
kernel_name <<< grid、block、sharedMemSize、stream >>>(引数リスト);
//デフォルト以外のストリーム宣言 cudaStream_tstream; // cudaStreamCreate(&stream); //リソースリリースを初期化します cudaError_tcudaStreamDestroy(cudaStream_t stream);
リソース解放を実行するときに、ストリームで実行されていない作業がまだある場合、関数はすぐに戻りますが、これらのリソースは、関連する作業が完了すると自動的に解放されます。
すべてのstramの実行は非同期であるため、必要に応じて同期操作を実行するために一部のAPIが必要です。
cudaError_t cudaStreamSynchronize(cudaStream_t stream); cudaError_t cudaStreamQuery(cudaStream_t stream);
1つ目は、ホストに強制的にブロックし、ストリーム内のすべての操作が完了するまで待機します。2つ目は、ストリーム内のすべての操作が完了したかどうかを確認し、操作が完了していなくてもホストをブロックしません。すべての操作が完了した場合はcudaSuccessを返し、そうでない場合はcudaErrorNotReadyを返します。
理解に役立つコードスニペットを見てみましょう。
for(int i = 0; i <nStreams; i ++){ int offset = i * bytesPerStream; cudaMemcpyAsync(&d_a [offset]、&a [offset]、bytePerStream、streams [i]); カーネル<<グリッド、ブロック、0、ストリーム[i] >>(&d_a [オフセット]); cudaMemcpyAsync(&a [offset]、&d_a [offset]、bytesPerStream、streams [i]); } for(int i = 0; i <nStreams; i ++){ cudaStreamSynchronize(streams [i]); }
このコードは3つのストリームを使用し、データ転送とカーネル操作はこれらの同時ストリームに割り当てられます。
上の写真はパイプラインと同じなので、これ以上は言いません。上図のデータ送信操作は、ストリームが異なっていても並行して実行されないことに注意してください。慣例により、この状況は間違いなくハードウェアリソースのポットです。ハードウェアリソースはごくわずかです。ソフトウェアレベルでの最適化は、すべてのハードウェアリソースをノンストップで使用できるようにすることに他なりません(邪悪な資本主義、ええと。 。)、そしてこれがPCIeカードのボトルネックです。もちろん、プログラミングの観点からは、これらの操作は互いに独立していますが、ハードウェアリソースを共有する場合は、シリアルである必要があります。2つのPCIeは、これら2つのデータ送信操作をオーバーラップさせることができますが、異なるストリームと異なる送信方向を確保する必要もあります。
同時カーネルの最大数は、デバイス自体によって異なります。Fermiは16の並列チャネルをサポートし、Keplerは32です。パラレルの数は、共有メモリ、レジスタ、およびその他のデバイスリソースによって制限されます。
ストリームスケジューリング
概念的には、すべてのストリームが同時に実行されます。ただし、これは通常は当てはまりません。
誤った依存関係
Fermiは最大16の並列チャネルをサポートしますが、物理的には、すべてのストリームがスケジューリングのためにハードウェア上の唯一のワークキューにパックされます。グリッドが実行用に選択されると、ランタイムはタスクの依存関係をチェックします。現在のタスクが依存している場合前のタスクはブロックされます。キューが1つしかないため、次のタスクが他のストリームのタスクであっても、次のタスクの後に待機が続きます。次の図に示すように:
CとP、およびRとXは異なるストリームにあるため並列にすることができますが、ABC、PQR、およびXYZは並列ではありません。たとえば、CとPはBが完了する前に待機しています。
Hyper-Q
ケプラーシリーズでは、疑似依存の状況が解決されました。Hyper-Qと呼ばれる技術が採用されています。単純で大雑把な理解では、作業キューが十分ではないため、増やす必要があるため、ケプラーには32の作業キューがあります。 。。このテクノロジーは、TPCでコンピューティングとグラフィックスを同時に実行できるアプリケーションも実現します。もちろん、32を超えるストリームが作成された場合でも、誤った依存関係が存在します。
ストリームの優先順位
CC3.5以降の場合、ストリームには次の優先度属性を設定できます。
cudaError_t cudaStreamCreateWithPriority(cudaStream_t * pStream、unsigned int flags、int priority);
この関数はストリームを作成し、優先度を優先します。優先度の高いグリッドは、優先度の低い実行をプリエンプトできます。ただし、priority属性はカーネルに対してのみ有効であり、データ送信に対しては有効ではありません。また、設定した優先度が設定可能な範囲を超えると、自動的に最高または最低に設定されます。有効な設定可能範囲は、次の機能で照会できます。
cudaError_t cudaDeviceGetStreamPriorityRange(int * leftPriority、int * greatestPriority);
名前が示すように、leastPriorityは下限であり、gretestPriorityは上限です。古いルールでは、値が小さいほど優先度が高くなります。デバイスが優先度設定をサポートしていない場合、これらの値は両方とも0を返します。
Cudaイベント
イベントはストリームに関連する重要な概念であり、streanの実行プロセスの特定のポイントをマークするために使用されます。その主な用途は次のとおりです。
- 同期ストリーム実行
- デバイスの実行ペースを制御する
Cuda apiは、イベントをストリームに挿入し、イベントが完了したかどうか(または、条件を満たしているかどうか)を照会するための関連関数を提供します。イベントでマークされたストリーム位置でのすべての操作が実行された場合にのみ、イベントは完了したと見なされます。デフォルトのストリームに関連付けられているイベントは、すべてのストリームに有効です。
作成と破壊
// cudaEvent_tイベントを宣言し ます; // cudaError_tを作成しますcudaEventCreate(cudaEvent_t * event); // cudaError_tを破棄しますcudaEventDestroy(cudaEvent_t event);
同様に、streeamの解放の場合、関数が呼び出されたときに、関連する操作が完了していなければ、操作の完了後にリソースが自動的に解放されます。
イベントの記録と経過時間の測定
イベントは、ストリーム実行プロセスのポイントをマークします。実行中のストリーム内の操作がそのポイントに到達したかどうかを確認できます。イベントを操作としてストリーム内の多くの操作に挿入できます。操作が実行されると、作業が完了します。 CPUのフラグを設定して、CPUが完了したことを示します。次の関数は、イベントを指定されたストリームに関連付けます。
cudaError_t cudaEventRecord(cudaEvent_t event、cudaStream_t stream = 0);
イベントを待機すると、呼び出し元のホストスレッドがブロックされ、同期操作によって次の関数が呼び出されます。
cudaError_t cudaEventSynchronize(cudaEvent_t event);
この関数はcudaStreamSynchronizeに似ていますが、ストリーム全体ではなくイベントが実行を終了するのを待つ点が異なります。次のAPIを使用して、イベントが完了したかどうかをテストすることもできます。この関数はホストをブロックしません。
cudaError_t cudaEventQuery(cudaEvent_t event);
この関数はcudaStreamQueryに似ています。さらに、2つのイベント間の時間間隔を測定するための専用APIがあります。
cudaError_t cudaEventElapsedTime(float * ms、cudaEvent_t start、cudaEvent_t stop);
開始と停止の間の時間間隔をミリ秒単位で返します。開始と停止を同じストリームに関連付ける必要はありませんが、2つのいずれかがNULL以外のストリームに関連付けられている場合、時間間隔が予想よりも長くなる可能性があることに注意してください。これは、cudaEventRecordが非同期で発生し、測定された時間が2つのイベントの間に正確にあることを保証できないためです。したがって、GPU作業間の時間間隔が必要なだけなので、stopとstratはデフォルトのストリームに関連付けられます。
次のコードは、イベントを使用して時間を測定する方法を示しています。
// 2つのイベントを作成します cudaEvent_tstart、stop; cudaEventCreate(&start); cudaEventCreate(&stop); //開始イベントをデフォルトのストリームに記録します cudaEventRecord(start); //カーネル kernel <<< grid、block >>>(arguments);を実行します //停止イベントをデフォルトストリームに記録します cudaEventRecord(stop); //停止イベントが完了するまで待ち ますcudaEventSynchronize(stop); // 2つのイベント間の経過時間を計算します floattime; cudaEventElapsedTime(&time、start、stop); // 2つのイベントをクリーンアップします cudaEventDestroy(start); cudaEventDestroy(stop);
ストリームの同期
デフォルト以外のすべてのストリーム操作はホストに対して非ブロッキングであるため、対応する同期操作が必要です。
ホストの観点から、cuda操作は2つのカテゴリに分類できます。
- メモリ関連の操作
- カーネルの起動
カーネルの起動はホストに対して非同期であり、cudaMemcpyなどの多くのメモリ操作は同期的です。ただし、cudaランタイムは、メモリ操作を実行するための非同期関数も提供します。
ストリームは、同期(NULLストリーム)と非同期(非NULLストリーム)の2つのタイプに分類できることはすでにわかっています。同期と非同期はホスト用です。非同期ストリームはホストの実行をブロックしませんが、ほとんどの同期ストリームはブロックします。ホストをブロックしますが、カーネル起動例外はホストをブロックしません。
さらに、非同期ストリームは、ブロッキングと非ブロッキングに分けることができます。ブロッキング非ブロッキングとは、同期ストリームの非同期ストリームを指します。非同期ストリームがブロッキングストリームである場合、同期ストリームは非同期ストリームの操作をブロックします。非同期ストリームが非ブロッキングストリームである場合、ストリームは同期ストリームの操作をブロックしません(少し回り道...)。
ブロッキングストリームと非ブロッキングストリーム
cudaStreamCreateを使用してブロッキングストリームを作成します。つまり、ストリームで実行される操作は、以前に実行された同期ストリームによってブロックされます。一般的に、NULLストリームが発行されると、cudaコンテキストはNULLストリームを実行する前に、前のすべてのブロッキングストリームの完了を待機します。もちろん、すべてのブロッキングストリームも、実行を開始する前に前のNULLストリームの完了を待機します。
例えば:
kernel_1 <<< 1、1、0、stream_1 >>>(); kernel_2 <<< 1、1 >>>(); kernel_3 <<< 1、1、0、stream_2 >>>();
デバイスの観点からは、これら3つのカーネルはシリアルに実行されますが、もちろん、ホストの観点からは、これらは並列で非ブロッキングです。cudaStreamCreateによって生成されたブロッキングストリームに加えて、次のAPI構成を介して非ブロッキングストリームを生成することもできます。
cudaError_t cudaStreamCreateWithFlags(cudaStream_t * pStream、unsigned int flags); //フラグには2つのタイプがあり、デフォルトは最初のフラグで、非ブロッキングは2番目のタイプです。 cudaStreamDefault:デフォルトのストリーム作成フラグ(ブロッキング) cudaStreamNonBlocking:非同期ストリーム作成フラグ(非ブロッキング)
以前のkernel_1およびkernel_3ストリームが2番目のタイプとして定義されている場合、それらはブロックされません。
暗黙的な同期
Cudaには、ホストとデバイス間の同期に、明示的と暗黙的の2種類があります。明示的な同期APIは次のとおりです。
- cudaDeviceSynchronize
- cudaStreamSynchronize
- cudaEventSynchronize
これらの3つの関数は、ホストによって明示的に呼び出され、デバイス上で実行されます。
暗黙的な同期についても学びました。たとえば、cudaMemcpyはデバイスとホストを暗黙的に同期します。この関数の同期機能はデータ送信の副作用にすぎないため、暗黙的と呼ばれます。このような関数を誤って呼び出すとパフォーマンスが大幅に低下する可能性があるため、これらの暗黙的な同期を理解することが重要です。
暗黙的な同期は、通常デバイス側で発生する予期しないブロッキング動作を引き起こす可能性があるため、cudaプログラミングの特殊なケースです。次のような多くのメモリ関連の操作は、現在のデバイスの操作に影響を与えます。
- ページロックされたホストメモリ割り当て
- デバイスのメモリ割り当て
- デバイスmemset
- 同じデバイス上の2つのアドレス間のメモリコピー
- L1 /共有メモリ構成の変更
明示的な同期
グリッドレベルからの明示的な同期方法は次のとおりです。
- デバイスの同期
- ストリームの同期
- ストリーム内のイベントの同期
- イベントを使用してストリーム間で同期する
前述のcudaDeviceSynchronizeを使用して、デバイス上のすべての操作を同期できます。この機能により、ホストはすべてのデバイスでの操作またはデータ転送操作の完了を待機します。明らかに、この関数は重量級の関数であり、そのような関数の使用を最小限に抑える必要があります。
cudaStreamSynchronizeを使用すると、特定のストリーム内のすべての操作が完了するのをホストに待機させるか、非ブロッキングバージョンのcudaStreamQueryを使用して完了したかどうかをテストできます。
Cudaイベントを使用すると、よりきめ細かいブロッキングと同期を実現できます。関連する関数はcudaEventSynchronizeとcudaEventSynchronizeであり、使用法はストリーム関連の関数と同様です。さらに、cudaStreamWaitEventは、ストリーム間の依存関係を導入するための柔軟な方法を提供します。
cudaError_t cudaStreamWaitEvent(cudaStream_t stream、cudaEvent_t event);
この関数は、ストリームが特定のイベントを待機することを指定します。イベントは、同じストリームまたは異なるストリームに関連付けることができます。次の図に示すように、異なるストリームの場合:
Stream2は、stream1のイベントが完了するのを待って、実行を続行します。
構成可能なイベント
イベントの構成では、次の機能を使用できます。
cudaError_t cudaEventCreateWithFlags(cudaEvent_t * event、unsigned int flags); cudaEventDefault cudaEventBlockingSync cudaEventDisableTiming cudaEventInterprocess
cudaEventBlockingSyncは、イベントがホストをブロックすることを示します。cudaEventSynchronizeのデフォルトの動作は、CPUクロックを使用してイベントステータスを常に照会することです。cudaEventBlockingSyncを使用すると、呼び出し元のスレッドはスリープ状態になり、イベントが完了するまで他のスレッドまたはプロセスに制御を移します。ただし、これによりCPUクロックが少し無駄になり、イベントの完了からスレッドのウェイクアップまでの時間も長くなります。
cudaEventDisableTimingは、イベントが同期にのみ使用でき、タイミングデータを記録する必要がないことを指定します。記録タイムスタンプの消費を破棄すると、cuudaStreamWaitEventおよびcudaEventQueryの呼び出しパフォーマンスを向上させることができます。
cudaEventInterprocessは、イベントをプロセス間イベントとして使用できることを指定します。
NVIDIA CUDAセクション:https://developer.nvidia.com/cuda-zone
CUDAオンラインドキュメント:http://docs.nvidia.com/cuda/index.html#
元のテキストを転載し、次のように示します:http://www.cnblogs.com/1024incn/p/5891051.html