web-dev-qa-db-ja.com

CUDAランタイムAPIを使用してエラーをチェックする標準的な方法は何ですか?

CUDAの質問に対する回答とコメント、および CUDAタグwiki を見ると、すべてのAPI呼び出しのリターンステータスでエラーをチェックすることが推奨されることがよくあります。 APIドキュメントには、cudaGetLastErrorcudaPeekAtLastErrorcudaGetErrorStringなどの関数が含まれていますが、これらを組み合わせて、多くの余分なコードを必要とせずにエラーを確実にキャッチおよび報告する最良の方法は何ですか?

239
talonmies

おそらく、ランタイムAPIコードのエラーをチェックする最良の方法は、次のようなアサートスタイルハンドラー関数とラッパーマクロを定義することです。

#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, const char *file, int line, bool abort=true)
{
   if (code != cudaSuccess) 
   {
      fprintf(stderr,"GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
      if (abort) exit(code);
   }
}

その後、gpuErrchkマクロで各API呼び出しをラップできます。このマクロは、ラップするAPI呼び出しの戻りステータスを処理します。次に例を示します。

gpuErrchk( cudaMalloc(&a_d, size*sizeof(int)) );

呼び出しにエラーがある場合、エラーと、エラーが発生したコード内のファイルと行を説明するテキストメッセージがstderrに出力され、アプリケーションが終了します。必要に応じて、より洗練されたアプリケーションでexit()を呼び出すのではなく、gpuAssertを変更して例外を発生させることができます。

2番目の関連する質問は、カーネルの起動時のエラーをチェックする方法です。これは、標準のランタイムAPI呼び出しのようなマクロ呼び出しで直接ラップすることはできません。カーネルの場合、次のようなものです。

kernel<<<1,1>>>(a);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );

最初に無効な起動引数をチェックしてから、カーネルが停止して実行エラーをチェックするまでホストを待機させます。このような後続のブロッキングAPI呼び出しがある場合、同期は削除できます。

kernel<<<1,1>>>(a_d);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaMemcpy(a_h, a_d, size * sizeof(int), cudaMemcpyDeviceToHost) );

この場合、cudaMemcpy呼び出しは、カーネルの実行中に発生したエラーまたはメモリコピー自体からのエラーを返すことができます。これは初心者にとって混乱を招く可能性があります。デバッグ中にカーネルを起動した後に明示的な同期を使用して、問題が発生する可能性のある場所を理解しやすくすることをお勧めします。

CUDA Dynamic Parallelism を使用する場合、デバイスカーネルでのCUDAランタイムAPIの使用、およびデバイスカーネルの起動後に、非常によく似た方法論を適用できることに注意してください。

#include <assert.h>
#define cdpErrchk(ans) { cdpAssert((ans), __FILE__, __LINE__); }
__device__ void cdpAssert(cudaError_t code, const char *file, int line, bool abort=true)
{
   if (code != cudaSuccess)
   {
      printf("GPU kernel assert: %s %s %d\n", cudaGetErrorString(code), file, line);
      if (abort) assert(0);
   }
}
281
talonmies

上記のtalonmiesの答えは、assertスタイルの方法でアプリケーションを中止する素晴らしい方法です。

大規模なアプリケーションの一部として、C++コンテキストのエラー状態を報告して回復したい場合があります。

以下は、std::runtime_errorを使用してthrust::system_errorから派生したC++例外をスローすることにより、それを行う合理的に簡潔な方法です。

#include <thrust/system_error.h>
#include <thrust/system/cuda/error.h>
#include <sstream>

void throw_on_cuda_error(cudaError_t code, const char *file, int line)
{
  if(code != cudaSuccess)
  {
    std::stringstream ss;
    ss << file << "(" << line << ")";
    std::string file_and_line;
    ss >> file_and_line;
    throw thrust::system_error(code, thrust::cuda_category(), file_and_line);
  }
}

これにより、cudaError_tのファイル名、行番号、および英語の説明が、スローされた例外の.what()メンバーに組み込まれます。

#include <iostream>

int main()
{
  try
  {
    // do something crazy
    throw_on_cuda_error(cudaSetDevice(-1), __FILE__, __LINE__);
  }
  catch(thrust::system_error &e)
  {
    std::cerr << "CUDA error after cudaSetDevice: " << e.what() << std::endl;

    // oops, recover
    cudaSetDevice(0);
  }

  return 0;
}

出力:

$ nvcc exception.cu -run
CUDA error after cudaSetDevice: exception.cu(23): invalid device ordinal

some_functionのクライアントは、必要に応じてCUDAエラーを他の種類のエラーと区別できます。

try
{
  // call some_function which may throw something
  some_function();
}
catch(thrust::system_error &e)
{
  std::cerr << "CUDA error during some_function: " << e.what() << std::endl;
}
catch(std::bad_alloc &e)
{
  std::cerr << "Bad memory allocation during some_function: " << e.what() << std::endl;
}
catch(std::runtime_error &e)
{
  std::cerr << "Runtime error during some_function: " << e.what() << std::endl;
}
catch(...)
{
  std::cerr << "Some other kind of error during some_function" << std::endl;

  // no idea what to do, so just rethrow the exception
  throw;
}

thrust::system_errorstd::runtime_errorであるため、前の例の精度を必要としない場合は、代わりに広範なエラーと同じ方法で処理できます。

try
{
  // call some_function which may throw something
  some_function();
}
catch(std::runtime_error &e)
{
  std::cerr << "Runtime error during some_function: " << e.what() << std::endl;
}
68
Jared Hoberock

C++標準の方法:エラーをチェックしないでください...例外をスローするC++バインディングを使用してください。

私はこの問題に悩まされていました。タロンミーズとジャレッドの答えのようにマクロ兼ラッパー機能のソリューションを使用していましたが、正直ですか? CUDAランタイムAPIの使用がさらにくなり、Cのようになります。

それで、私はこれに別のより基本的な方法でアプローチしました。結果のサンプルについては、CUDA vectorAddサンプルの一部です-すべてのランタイムAPI呼び出しのcompleteエラーチェック付き:

// (... prepare Host-side buffers here ...)

auto current_device = cuda::device::current::get();
auto d_A = cuda::memory::device::make_unique<float[]>(current_device, numElements);
auto d_B = cuda::memory::device::make_unique<float[]>(current_device, numElements);
auto d_C = cuda::memory::device::make_unique<float[]>(current_device, numElements);

cuda::memory::copy(d_A.get(), h_A.get(), size);
cuda::memory::copy(d_B.get(), h_B.get(), size);

// (... prepare a launch configuration here... )
cuda::launch( vectorAdd, launch_config,
    d_A.get(), d_B.get(), d_C.get(), numElements
);    
cuda::memory::copy(h_C.get(), d_C.get(), size);

// (... verify results here...)

繰り返しますが、すべての潜在的なエラーは、スローされた例外を介してチェックおよび報告されます。このコードは

CUDAランタイムAPIライブラリのThin Modern-C++ラッパー (Github)

例外には、失敗した呼び出しの後、文字列の説明とCUDAランタイムAPIステータスコードの両方が含まれていることに注意してください。

これらのラッパーを使用してCUDAエラーを自動的にチェックする方法へのいくつかのリンク:

20
einpoklum

議論された解決策は here 私にとってはうまくいった。このソリューションは組み込みのcuda関数を使用し、実装が非常に簡単です。

関連するコードを以下にコピーします。

#include <stdio.h>
#include <stdlib.h>

__global__ void foo(int *ptr)
{
  *ptr = 7;
}

int main(void)
{
  foo<<<1,1>>>(0);

  // make the Host block until the device is finished with foo
  cudaDeviceSynchronize();

  // check for error
  cudaError_t error = cudaGetLastError();
  if(error != cudaSuccess)
  {
    // print the CUDA error message and exit
    printf("CUDA error: %s\n", cudaGetErrorString(error));
    exit(-1);
  }

  return 0;
}
7
jthomas