web-dev-qa-db-ja.com

OpenCL 2.0デバイスコマンドキューがいっぱいになり、実行が停止する

OpenCLのenqueue_kernel()関数を使用して、GPUから動的にカーネルをエンキューして、不要なホストの相互作用を減らしています。これが私がカーネルでやろうとしていることの簡単な例です:

kernel void kernelA(args)
{
    //This kernel is the one that is enqueued from the Host, with only one work item. This kernel
    //could be considered the "master" kernel that controls the logic of when to enqueue tasks
    //First, it checks if a condition is met, then it enqueues kernelB

    if (some condition)
    {
        enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(some amount, 256), ^{kernelB(args);});
    }
    else
    {
        //do other things
    }
}

kernel void kernelB(args)
{
    //Do some stuff

    //Only enqueue the next kernel with the first work item. I do this because the things
    //occurring in kernelC rely on the things that kernelB does, so it must take place after kernelB is completed,
    //hence, the CLK_ENQUEUE_FLAGS_WAIT_KERNEL
    if (get_global_id(0) == 0)
    {
        enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(some amount, 256), ^{kernelC(args);});
    }
}

kernel void kernelC(args)
{
    //Do some stuff. This one in particular is one step in a sorting algorithm

    //This kernel will enqueue kernelD if a condition is met, otherwise it will
    //return to kernelA 
    if (get_global_id(0) == 0 && other requirements)
    {
        enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(1, 1), ^{kernelD(args);});
    }
    else if (get_global_id(0) == 0)
    {
        enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(1, 1), ^{kernelA(args);});
    }
}

kernel void kernelD(args)
{
    //Do some stuff

    //Finally, if some condition is met, enqueue kernelC again. What this will do is it will
    //bounce back and forth between kernelC and kernelD until the condition is
    //no longer met. If it isn't met, go back to kernelA
    if (some condition)
    {
        enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(some amount, 256), ^{kernelC(args);});
    }
    else
    {
        enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(1, 1), ^{kernelA(args);});
    }
}

これがプログラムの一般的なフローであり、1つの問題を除いて、意図したとおりの順序で、完全に機能し、意図したとおりに機能します。ワークロードが非常に高い特定のケースでは、enqueue_kernel()のランダムな1つがプログラムのエンキューと停止に失敗します。これは、デバイスキューがいっぱいになり、別のタスクをそのキューに入れることができないために発生します。しかし、私が一生懸命研究した後でも、これがなぜなのかを理解することはできません。

キュー内のタスク(たとえばカーネル)が完了すると、キュー内のその場所が解放されると思いました。したがって、私のキューは一度に1つまたは2つのタスクの最大値に到達するだけです。しかし、このプログラムは文字通り、デバイスコマンドキューの262,144バイトサイズ全体を埋め、機能を停止します。

誰かが何かアイデアを持っているのなら、なぜこれが起こっているのかについていくつかの潜在的な洞察をいただければ幸いです。私は一種の行き詰まりがあり、この問題を乗り越えるまで続行できません。

前もって感謝します!

(ところで私はRadeon RX 590カードで実行していて、AMD APP SDK 3.0を使用してOpenCL 2.0で使用しています)

3
Porter Morgan

何が問題なのか正確にはわかりませんが、投稿したコードにいくつかの点に気づきました。このフィードバックはコメントが長すぎたり、読みづらいので、明確な答えではありませんが、少し近づく:

コードはコメントが言うことをまったく行いません

kernelDには、次のものがあります。

_//Finally, if some condition is met, enqueue kernelC again.
_

_if (get_global_id(0) == 0)
{
    enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(some amount, 256), ^{kernelD(args);});
}
_

コメントが示唆するように、これは実際にkernelDではなくkernelC自体を再びキューに入れます。他の条件分岐はkernelAをエンキューします。

これは、コードの縮小版のタイプミスかもしれません。

潜在的なタスク爆発

これもまた、コードを要約した方法に影響する可能性がありますが、その方法はよくわかりません

したがって、私のキューは一度に1つまたは2つのタスクの最大値に到達するだけです。

本当であることができます。私の読書では、kernelCkernelDの両方のすべての作業項目が新しいタスクを生成します。いずれの場合も複数の作業項目があるように見えるため、これは非常に多数のタスクを簡単に生成できるように見えます。

たとえば、kernelCの場合:

_if (get_global_id(0) == 0 && other requirements)
{
    enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(some amount, 256), ^{kernelD(args);});
}
else
{
    enqueue_kernel(get_default_queue(), CLK_ENQUEUE_FLAGS_WAIT_KERNEL, ndrange_1D(1, 1), ^{kernelA(args);});
}
_

kernelBは、kernelCを実行する少なくとも256個の作業項目を作成します。ここで、ワークアイテム0は(_other requirements_が満たされた場合)少なくとも256個のワークアイテムを含む1つのタスクと、1つのワークアイテムがkernelAを実行する255以上のタスクを生成します。 kernelDも同様に動作します。

したがって、数回の反復で、キューに入れられたkernelAを実行するための数千のタスクが簡単に発生する可能性があります。私はあなたのコードが何をしているのか本当にわかりませんが、これらの何百ものkernelAタスクを削減することで状況が改善されるかどうか、そしておそらくkernelAを変更できるかどうかを確認するのは良い考えのようです。すべての作業項目から作業サイズ1をエンキューするのではなく、範囲を指定して1回エンキューするだけです。 (または、これらの行に沿って何か-それが理にかなっている場合、おそらくグループごとに1回エンキューします。基本的に、_enqueue_kernel_が呼び出される回数を減らします。)

enqueue_kernel()戻り値

_enqueue_kernel_の戻り値を実際に確認しましたか?それが失敗した理由を正確に教えてくれるので、上記の提案が不可能な場合でも、タスクが中断された場合に、もう1つのタスクが排出されたら、kernelAが計算を再開できるようにするグローバル状態を設定できますか?

2
pmdj