SYCL* 例外ハンドラーを使用する

Data Parallel C++ Mastering DPC++ for Programming of Heterogeneous Systems using C++ and SYCL』 (英語) の書籍では次のことが説明されています:

C++ の例外機能は、プログラム内でエラーが検出された位置と、エラーが処理される位置は明確に分離するように設計されており、この概念は SYCL* の同期エラーと非同期エラーの両方にも適合します。

この書籍で推奨されるメソッドを使用すると、C++ 例外処理は、エラーが発生するとプログラムが何の通知もなく終了するのではなく、なんらかの通知を行って終了するのに役立ちます。

: この節のイタリックのテキストは、C++ と SYCL* を使用するヘテロジニアス・システムのプログラミングを説明する『Data Parallel C++ Mastering DPC++ for Programming of Heterogeneous Systems using C++ and SYCL.*』の第 5 章 “Error Handling” から直接コピーしたものです。一部の説明では、簡素化のためテキストが省略されています。詳細は書籍を参照してください。

エラー処理の無視

C++ と SYCL* では、エラーを明示的に処理しなくても問題が発生したことを通知できるように設計されています。未処理の同期または非同期のエラーのデフォルトの結果は、オペレーティング・システムが通知するプログラムの異常終了になります。次の 2 つの例は、それぞれ同期エラーと非同期エラーを処理しない場合に発生する動作を模倣します。以下のコード例は、ハンドルされていない C++ 例外の結果を示しています。これは、ハンドルされていない SYCL* 同期エラーなどが原因である可能性があります。このコードでは、特定のオペレーティング・システムがどのように動作するかテストできます。

C++ の未処理例外

#include <iostream> 
class something_went_wrong {}; 
int main() { 
  std::cout << "Hello\n"; 
  throw(something_went_wrong{}); 
} 
Example output in Linux: 
Hello 
terminate called after throwing an instance of 'something_went_wrong' 
Aborted (core dumped)

以下は、呼び出された std::terminate からの出力例を示します。これは、アプリケーションで未処理の SYCL* 非同期エラーが発生した結果です。このコードでは、特定のオペレーティング・システムがどのように動作するかテストできます。プログラムでエラーを処理する必要がありますが、キャッチされないエラーはキャッチされてプログラムが終了するため、プログラムが何も通知することなく失敗するのを心配する必要がありません。

std::terminate は SYCL* 非同期例外が処理されないときに呼び出されます。

#include <iostream> 
int main() { 
   std::cout << "Hello\n"; 
   std::terminate(); 
} 
Example output in Linux: 
Hello 
terminate called without an active exception 
Aborted (core dumped)

ここでは、同期エラーを C++ 例外によって処理できる理由について詳しく説明します。ただし、アプリケーションの制御ポイントで非同期エラーを処理するには、SYCL* スローが呼び出される状況を認識し、それに応じて SYCL* 例外を使用する必要があります。

SYCL* で定義される同期エラーは、``sycl::exception`` タイプからの派生クラスであり、以下に示すように try-catch 構造によって SYCL* エラーをキャッチできます。

sycl::exception をキャッチするパターン

try{{ 
  // SYCL のワークを実行 
} catch (sycl::exception &e) { 
  // 例外を出力または処理 
  std::cout << "Caught sync SYCL exception: " << e.what() << "\n"; 
  return 1; 
}

C++ のエラー処理メカニズムに加えて、SYCL* ではランタイムによってスローされる * ``sycl::exception`` * 例外タイプを追加します。それ以外はすべて標準 C++ の例外処理であるため、開発者にはなじみ深いものです。さらに詳しい例を以下に示します。ここでは、追加の例外クラスが処理され、main() からリターンすることでプログラムが終了します。C++ のエラー処理メカニズムに加えて、SYCL* ではランタイムによってスローされる * ``sycl::exception`` * 例外タイプを追加します。それ以外はすべて標準 C++ の例外処理であるため、開発者にはなじみ深いものです。さらに詳しい例を以下に示します。ここでは、追加の例外クラスが処理され、main() からリターンすることでプログラムが終了します。

コードブロックから例外をキャッチするパターン。

try{{ 
  buffer<int> B{ range{16} }; 
  // エラー: 親バッファのサイズより大きいサブバッファーを作成  
  // バッファー・コンストラクタ内から例外がスロー  
  buffer<int> B2(B, id{8}, range{16}); 
} catch (sycl::exception &e) { 
  // 例外を出力または処理  
  std::cout << "Caught sync SYCL exception: " << e.what() << "\n"; 
  return 1; 
} catch (std::exception &e) { 
  std::cout << "Caught std exception: " << e.what() << "\n"; 
  return 2; 
} catch (...){ 
  std::cout << "Caught unknown exception\n"; 
  return 3; 
} 
return 0; 
出力例: 
Caught sync SYCL exception: Requested sub-buffer size exceeds the size of the parent buffer -30 (CL_INVALID_VALUE)

非同期エラー処理

非同期エラーは SYCL* ランタイム (またはベースとなるバックエンド) によって検出され、エラーはホストプログラムのコマンドの実行とは無関係に発生します。エラーは SYCL* ランタイムの内部リストに格納され、プログラマーが制御できる特定の位置でのみ処理を行うためリリースされます。非同期エラーの処理をカバーするのに次の 2 つのことを理解してください。 1. 非同期ハンドラーは、処理すべき未処理の非同期エラーがある場合に呼び出される非同期ハンドラーです。 2. 非同期ハンドラーが呼び出されると、非同期ハンドルが呼び出されます。 非同期ハンドラーはアプリケーションが定義する関数であり、SYCL* コンテキストやキューに登録されます。次のセクションで定義される時間に、処理可能な未処理の非同期例外がある場合、SYCL* ランタイムによって非同期ハンドラーが呼び出されて例外リストが渡されます。非同期ハンドラーは、std::function としてコンテキストまたはキュー・コンストラクターに渡され、好みに応じて通常の関数、ラムダ、またはファンクターなどの方法で定義できます。ハンドラーは、以下の図に示すサンプルのように、sycl::exception_list 引数を受け入れる必要があります。

ラムダとして定義された非同期ハンドラーの実装例

// シンプルな非同期ハンドラー関数 
auto handle_async_error = [](exception_list elist) { 
  for (auto &e : elist) { 
    try{ std::rethrow_exception(e); } 
    catch ( sycl::exception& e ) { 
      std::cout << "ASYNC EXCEPTION!!\n"; 
      std::cout << e.what() << "\n"; 
    } 
  } 
};

上の図では、std::rethrow_exception の後に特定の例外タイプの catch が続くことで、例外タイプ (この場合は sycl::exception のみ) のフィルター処理が行われます。C++ では代替のフィルター処理手法を使用することも、タイプに関係なくすべての例外を処理することもできます。ハンドラーは構築時にキューまたはコンテキスト (低レベルの詳細については第 6 章で説明します) に関連付けられます。たとえば、上の図で定義されているハンドラーを、作成しているキューに登録するには、queue my_queue{ gpu_selector{}, handle_async_error } ように記述します。 同様に、上の図で定義されているハンドラーを、作成しているコンテキストに登録するには、context my_context{ handle_async_error } のように記述します。ほとんどのアプリケーションでは、コンテキストを明示的に作成または管理する必要はありません (コンテキストは自動的にバックグラウンドで作成されます)。そのため、非同期ハンドラーを使用する場合、開発者は、そのようなハンドラーを特定のデバイス用に構築されているキュー (明示的なコンテキストではない) に関連付ける必要があります。

: 非同期ハンドラーを定義する場合、何らかの理由でコンテキストを明示的に管理しない限り、キューで定義する必要があります。キューやその親コンテキストに対して非同期ハンドラーが定義されていないと、そのキュー (またはコンテキスト) で処理が必要な非同期エラーが発生した場合、デフォルトの非同期ハンドラーが呼び出されます。デフォルトのハンドラーは次のリストと同じように動作します。

デフォルトの非同期ハンドラーの動作例

// シンプルな非同期ハンドラー関数 
auto handle_async_error = [](exception_list elist) { 
  for (auto &e : elist) { 
    try{ std::rethrow_exception(e); } 
    catch ( sycl::exception& e ) { 
      // 非同期例外に関する情報をプリント 
    } 
  } 
  // 何か未処理の事象が発生したことを 
  // ユーザーに明確に伝えるため異常終了
  std::terminate(); 
};

デフォルトのハンドラーは、例外リスト内のエラー情報をユーザーに通知し、アプリケーションを異常終了させます。この場合、オペレーティング・システムも異常終了したことをレポートする必要があります。非同期ハンドラーにどのようなエラーを通知するかはプログラマーに依存します。アプリケーションが処理を正常に続行できるよう、エラーのログにはアプリケーションの終了、そしてエラー状態の回復までさまざまな情報があります。一般的なケースでは、sycl::exception::what() を呼び出してエラーの詳細を報告し、その後アプリケーションを終了します。非同期ハンドラーが内部で何を処理するかを決定するのはプログラマー次第ですが、しばしば見られる間違いとして、エラーメッセージ (プログラムのほかのメッセージで見逃される可能性があります) を出力してから、ハンドラー関数を終了することです。プログラムの状態を回復して、実行を継続しても安全であると確信できるようエラー管理が成されていない限り、非同期ハンドラー関数内でアプリケーションを終了することを検討してください。これにより、エラーが検出されたにもかかわらずアプリケーションの不正実行を続行するプログラムから誤った結果が表示される可能性が減少します。ほとんどのプログラムでは、非同期例外が発生したら異常終了することが適切です。

例: ゼロサイズのオブジェクトへの SYCL* スロー

次のサンプルコードは、サイズがゼロのオブジェクトが渡されたときに SYCL* ハンドラーがどのようにエラーを生成するかを示しています。

#include <cstdio> 
#include <CL/sycl.hpp> 

template <bool non_empty> 
static void fill(sycl::buffer<int> buf, sycl::queue & q) { 
    q.submit([&](sycl::handler & h) { 
        auto acc = sycl::accessor { buf, h, sycl::read_write }; 
        h.single_task([=]() { 
            if constexpr(non_empty) { 
                acc[0] = 1; 
            } 
        } 
        ); 
    } 
    ); 
    q.wait(); 
} 

int main(int argc, char *argv[]) { 
    sycl::queue q; 
    sycl::buffer<int, 1> buf_zero ( 0 ); 

    fprintf(stderr, "buf_zero.count() = %zu\n", buf_zero.get_count()); 
    fill<false>(buf_zero, q); 
    fprintf(stdout, "PASS\n"); 

    return 0; 
}

アプリケーションが実行中にサイズがゼロのオブジェクトに遭遇すると、プログラムはアボートし、エラーメッセージが出力されます:

$ dpcpp zero.cpp 
$ ./a.out 
buf_zero.count() = 0 
submit... 
terminate called after throwing an instance of 'cl::sycl::invalid_object_error' 
what(): SYCL buffer size is zero.To create a device accessor, SYCL buffer size must be greater than zero.-30 (CL_INVALID_VALUE) 
Aborted (core dumped)

プログラマーは、デバッガーで例外をキャッチし、エラーを招いたソース行のバックトレースを調査することで、プログラミングの問題を特定できます。

例: 不正な NULL ポインターに対する SYCL* スロー

以下のコードを考えてみます:

deviceQueue.memset(mdlReal, 0, mdlXYZ \* sizeof(XFLOAT)); 

deviceQueue.memcpy(mdlImag, 0, mdlXYZ \* sizeof(XFLOAT)); // コーディングエラー

コンパイラーは、deviceQueue.memcpy で指定された不正な (NULL ポインター) 値のフラグをセットしません。このエラーは、実行されるまでキャッチされません。

terminate called after throwing an instance of 'cl::sycl::runtime_error' 

what(): NULL pointer argument in memory copy operation.-30 
(CL_INVALID_VALUE) 

Aborted (core dumped)

次のコード例は、NULL ポインターのエラーを示すプログラムに実装された、特定キューでの実行時の例外出力が検出された際に、ユーザーが例外出力の形式を制御する方法を示しています。

#include "stdlib.h" 
#include "stdio.h" 
#include <cmath> 
#include <signal.h> 
#include <fstream> 
#include <iostream> 
#include <vector> 
#include <CL/sycl.hpp> 

#define XFLOAT float 
#define mdlXYZ 1000 
#define MEM_ALIGN 64 

int main(int argc, char *argv[]) { 
    XFLOAT *mdlReal, *mdlImag;
 
    cl::sycl::property_list propList = cl::sycl::property_list{cl::sycl::property::queue::enable_profiling()}; cl::sycl::queue deviceQueue(cl::sycl::gpu_selector { }, [&](cl::sycl::exception_list eL) 
    { 
        bool error = false; 
        for (auto e : eL) 
        { 
            try 
            { 
                std::rethrow_exception(e); 
            } catch (const cl::sycl::exception& e) 
            { 
                auto clError = e.get_cl_code(); 
                bool hascontext = e.has_context(); 
                std::cout << e.what() << "CL ERROR CODE : " << clError << std::endl; 
                error = true; 
                if (hascontext) 
                { 
                    std::cout << "We got a context with this exception" << std::endl; 
                } 
            } 
        } 
        if (error) { 
            throw std::runtime_error("SYCL errors detected"); 
    } }, propList); 

    mdlReal = sycl::malloc_device<XFLOAT>(mdlXYZ, deviceQueue); 
    mdlImag = sycl::malloc_device<XFLOAT>(mdlXYZ, deviceQueue); 

    deviceQueue.memset(mdlReal, 0, mdlXYZ * sizeof(XFLOAT)); 
    deviceQueue.memcpy(mdlImag, 0, mdlXYZ * sizeof(XFLOAT)); // コーディングエラー 

    deviceQueue.wait(); 

    exit(0); 
}

リソース

SYCL* API の誤った使用による SYCL* 例外をデバッグするガイド付きのアプローチについては、「ガイド付き行列乗算の例外のサンプル」 (英語) を参照してください。

リソースをオフロードする拡張機能を備えた OpenMP* または SYCL* API を使用するアプリケーションのトラブルシューティングは、「高度な並列アプリケーションのトラブルシューティング」(英語) のチュートリアルを参照してください。