カーネルコンパイラに、スレッドブロック(ワークグループ)内のスレッド(ワークアイテム)の数に関する追加情報を提供すると役立つことがよくあります。 たとえば、次のように、HIPコンパイラにスレッドブロック内のスレッド数の上限(たとえば1024)を与えることができます。
__launch_bounds__(1024) __global__ void fooKernel(...) { ... }
実際、現在のHIPリリースでは、残念ながら、スレッドブロックサイズが256を超える場合にこれを指定する必要があります(https://github.com/ROCm-Developer-Tools/HIP/issues/1310を参照)。
CUDAにも同じ属性があります。 スレッドブロックの最小数の起動境界修飾子には、追加の引数もあります(https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#launch-bounds)。
OpenCLのカーネル修飾子はわずかに異なります(https://www.khronos.org/registry/OpenCL/specs/2.2/pdf/OpenCL_C.pdfの6.7.2を参照)。
提案v1-コンパイル時にスレッドブロックサイズがわかっている理想的な世界では、OCCAは適切な起動境界(CUDA、HIP)またはワークグループサイズヒント(OpenCL)のカーネル修飾子を追加します。
プロポーザルv2-プロポーザルv1を実行するのが複雑すぎる場合は、起動境界@qualifier( "inner size"、B)にokl属性を追加するのが適切です。ここで、Bはコンパイラー定義です。 これは、CUDA / HIPの場合は__launch_bounds __(Bの値)に、OpenCLの場合は__attribute __((work_group_size_hint(Bの値)))に拡張されます。 マルチディムバリアントも役立ちます。
これはHIP / CUDA / OpenCLに固有のように思われるので、ビルドプロパティとして渡すのはどうですか?
addVectors = device.buildKernel("addVectors.okl",
"addVectors",
"launch_bounds: 1024");
それはいいことだと思います。
私はちょうどこれに遭遇しました。 これに貢献できてうれしいですが、質問があります。 OCCAはJIT時に内部ループの寸法を知っていますか?
ループの次元はカーネル引数として渡すことができると思います。
それは本当ですが、なぜそれが役立つのかわかりません。 JIT時にoccaが__launch_bounds__
属性を出力する方法を尋ねています。 その時点でのループの寸法を知る必要があります。 これはカーネルの引数と直交していますね。
彼は、内部ループの薄暗い部分を引数として渡すことができると言っています。つまり、JITコンパイル時にスレッドブロックの次元が必ずしもわからないということです。
そうそう。 あなたが今言っていることがわかります@tcew。 いい視点ね。
これは私が念頭に置いていたものです:
<strong i="6">@kernel</strong> void runtimeArgs(const int B,
const int T,
const int N,
const float *x,
const float *y,
float *xPy) {
for (int b=0;b<B;++b;@outer(0)){
for (int t=0;t<T;++t;@inner(0)){
if(b==0 && t==0) printf("B=%d, T=%d\n", B, T);
int n = t + T*b;
if(n<N){
xPy[n] = x[n] + y[n];
}
}
}
}
OCCAは明らかにJIT時の数値ループ境界を知ることができません。
ただし、スレッドグリッドの寸法を設定するランチャーは作成されます。
extern "C" void runtimeArgs(occa::modeKernel_t * *deviceKernel,
const int & B,
const int & T,
const int & N,
occa::modeMemory_t * x,
occa::modeMemory_t * y,
occa::modeMemory_t * xPy) {
{
occa::dim outer, inner;
outer.dims = 1;
inner.dims = 1;
int b = 0;
outer[0] = B - 0;
int t = 0;
inner[0] = T - 0;
occa::kernel kernel(deviceKernel[0]);
kernel.setRunDims(outer, inner);
kernel(B, T, N, x, y, xPy);
}
}
したがって、ユーザーは実行時にループ境界の任意のサイズを指定できます。
交差した投稿。
SYCLは同様の構文を使用します。
sycl::range<2> global_range(Bx*Tx,By*Ty);
sycl::range<2> local_range(Tx,Ty);
sycl::nd_range<2> kernel_range(global_range, local_range);
device_queue.submit([&](sycl::handler &cgh) {
...
cgh.parallel_for(kernel_range, kernel);
});
(バッファーには範囲が関連付けられているため、Nを渡す必要がありません)
SYCL sycl::range
構文はOpenCLから採用されており、OpenCL自体はCUDAから採用されています。
あなたの例では、ねじ切り寸法の仕様は、並列forループの本体とは別のものです。
OCCA OKL構文は、ループの次元と本体コードを、より馴染みのある並列forループ構文にするように特別に設計されています。
for (int b=0;b<B;++b;@outer(0)){ /* grid dimension defined here */
for (int t=0;t<T;++t;@inner(0)){ /* thread block dimension defined here */
if(b==0 && t==0) printf("B=%d, T=%d\n", B, T);
int n = t + T*b;
if(n<N){
xPy[n] = x[n] + y[n];
}
}
}
カーネル内のコードは、並列forループの境界を並列forループの本体に近接させておくことになっています。 また、より一般的なカーネルではデータ配列とは非常に異なるスレッドグリッド構成が必要になる場合があるため、ループ境界は入力配列によって指定されません。
OKLカーネル構造は、人々をトレーニングするときにCUDA / OpenCLカーネル構文、カーネル起動パラメーター、およびカーネルスレッド哲学を繰り返し説明しなければならないことから生まれた意図的な選択でした。
SYCLには、ここで説明しているCUDA / HIPの__launch_bounds__
のような類似のスレッドブロックサイズのヒントがありますか?
良い質問。 SYCL標準(v1.2.1)を再確認して調べました。 OpenCL Cで使用可能なすべての属性がサポートされており、 cl
名前空間を使用してC ++ 11属性指定子で指定できます。 たとえば、OpenCL Cの__attribute__(((reqd_work_group_size(n)))
$は、SYCLの[[cl::reqd_work_group_size(n)]]
と同等です。
スレッドグループのサイズを指定するために使用できるフレーバーは2つあります。 work_group_size_hint(n)
はソフトバージョンです。スレッドグループのサイズはn
になることをお勧めしますが、 req_work_group_size(n)
は厳密な要件です。 。
OCCAのいくつかのオプション:
「innerDimHint」メンバー関数をocca :: kernelクラスに追加します。これにより、CUDA / OpenCL / HIPのスレッドdimヒントを使用して(まだハッシュに含まれていない場合は)再コンパイルが強制されます。
ランチャー内にロジックを追加して、新しいスレッド配列サイズが指定されたときに実行される再コンパイルをトリガーします。 この機能は、一部のOCCA_ *定義によってオン/オフされる場合があります。
これらは両方とも、下位互換性のある方法で実行できます。
私は実際にこれを管理するために別々のランタイムを信頼し、何も再コンパイルしないことを選択します。 本質的には、 @ dmed256の元の提案を使用してビルドプロップにし、バックエンドがサポートしている場合は、翻訳時にそれぞれの__launch_bound__
ヒントをカーネルに追加します。
CUDAとHIPの場合、 __launch_bound__
は実際には単なるヒントなので、OpenCLのwork_group_size_hint
に似ているかもしれません。 これは、ブロック内の各スレッドで使用可能になると想定できるレジスターの数をコンパイラーに通知するためにのみ使用されます。 ユーザーが起動境界に違反した場合、カーネルがレジスターを頻繁に使用していない可能性があるため、必ずしもエラーではありません。 ユーザーが起動境界に違反し、実際に十分なレジスタがない場合、ランタイムはOCCAがキャッチする必要のあるエラーをスローします。
自動チューナーを実行しているときに、LDSまたはREGが多すぎるOCCA:HIPカーネルのセグメンテーション違反に気づきました。 私は、HIPがキャッチされるエラーをスローすることに懐疑的でした。 うまくいけば、それは今修正されています。
ユーザー提供のヒント、または実行時に新しいスレッド構成を検出するランチャーで問題ありません。
参考までに、私は、OKLカーネルコードのマクロ定数置換(たとえば、外部の場合はK_blockDim
、内部の場合はK_threadDim
)を介して、ホストコードから明示的なランタイムループ境界を指定するだけで満足しています。カーネルの小道具を介して。 もちろん、GPUの特性とステータスを照会する独自のAPIコードがあります。 その情報を使用して、渡すループ境界を計算します。 はい、そのような動的なことにより、実行時にJITが再コンパイルされることがありますが、私の場合、ステップしきい値の変更のためにまれであるため、実際のループ境界は通常同じままであるか、特定のカーネルの共通セットに分類されます。 また、一部のカーネルにはプリコンパイルを利用しているため、JITの再コンパイルも削減されます。
実行時にこれらのループ境界を明示的に定義すると、OKLコードでそれらを使用してGPUローカルメモリ配列の実行時サイジングも容易になります。
この機能が重要になるようです。
https://rocmdocs.amd.com/en/latest/Current_Release_Notes/Current-Release-Notes.html#performance -impact-for-kernel-launch-bound-attribute
不器用な実装についてお詫びしますが、これは、Np(> 256)スレッドを必要とするlibparanumalのカーネルに使用する回避策です。
occa::properties kernelInfo;
...
if(platform.device.mode()=="HIP"){
char newflag[BUFSIZ];
sprintf(newflag, " --gpu-max-threads-per-block=%d", mesh.Np);
kernelInfo["compiler_flags"] += newflag;
}
起動境界を指定するためにhipccコンパイラフラグを使用することは、NoelChalmersによって提案されました。 全体的な実装は私のものです。
カーネルが最大境界に違反した場合に何が起こるかは不明であるため、これを行うときは注意することが重要です。
不適切な境界の使用を避けるために、異なる最大スレッド数を使用するカーネル用にocca::properties
オブジェクトの個別のコピーを作成します。
将来的には、起動境界に違反するとランタイムエラーになる可能性があります。