Qual è il modo canonico per verificare la presenza di errori utilizzando l'API di runtime CUDA?


265

Esaminando le risposte ei commenti alle domande CUDA e nel wiki del tag CUDA , vedo che spesso viene suggerito che lo stato di ritorno di ogni chiamata API debba essere controllato per errori. La documentazione API contiene funzioni come cudaGetLastError, cudaPeekAtLastErrore cudaGetErrorString, ma qual è il modo migliore per mettere questi insieme ad errori di cattura e di report in modo affidabile senza richiedere un sacco di codice aggiuntivo?


15
Gli esempi CUDA di NVIDIA contengono un'intestazione, helper_cuda.h, che ha macro chiamate getLastCudaErrore checkCudaErrors, che fanno più o meno ciò che è descritto nella risposta accettata . Guarda gli esempi per le dimostrazioni. Basta scegliere di installare gli esempi insieme al toolkit e lo avrai.
chappjc

@chappjc Non penso che questa domanda e risposta pretendano di essere originali, se questo è ciò che intendi, ma ha il merito di aver istruito persone che utilizzano il controllo degli errori CUDA.
JackOLantern

@ JackOLantern No, non è quello che stavo insinuando. Questa domanda e risposta mi è stata molto utile ed è sicuramente più facile da trovare rispetto a qualche intestazione nell'SDK. Ho pensato che fosse utile sottolineare che questo è anche il modo in cui NVIDIA lo gestisce e dove cercare di più. Tuttavia ammorbidirei il tono del mio commento se potessi. :)
chappjc

Gli strumenti di debug che ti consentono di "avvicinarti" al punto in cui iniziano gli errori sono migliorati molto dal 2012 su CUDA. Non ho lavorato con debugger basati su GUI, ma il tag wiki CUDA menziona la riga di comando cuda-gdb. Questo è uno strumento molto potente in quanto ti permette di passo attraverso orditi reali e le discussioni sulla GPU in sé (richiede 2.0+ architettura maggior parte del tempo però)
opetrenko

@ bluefeet: qual è stato l'accordo con la modifica che hai annullato? Sembrava che nulla fosse effettivamente cambiato nel markdown, ma è stato accettato come modifica. C'era qualcosa di nefasto al lavoro?
talonmies

Risposte:


315

Probabilmente il modo migliore per verificare la presenza di errori nel codice API di runtime è definire una funzione di gestore dello stile di asserzione e una macro wrapper come questa:

#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);
   }
}

Puoi quindi racchiudere ogni chiamata API con la gpuErrchkmacro, che elaborerà lo stato di ritorno della chiamata API che avvolge, ad esempio:

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

Se si verifica un errore in una chiamata, verrà emesso un messaggio di testo che descrive l'errore e il file e la riga nel codice in cui si è verificato l'errore stderre l'applicazione verrà chiusa. Si potrebbe plausibilmente modificare gpuAssertper sollevare un'eccezione piuttosto che chiamare exit()un'applicazione più sofisticata se fosse necessario.

Una seconda domanda correlata è come verificare la presenza di errori nei lanci del kernel, che non possono essere inseriti direttamente in una chiamata macro come le chiamate API di runtime standard. Per i kernel, qualcosa del genere:

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

per prima cosa verificherà la presenza di argomenti di avvio non validi, quindi costringerà l'host ad attendere fino a quando il kernel si arresta e verifica la presenza di un errore di esecuzione. La sincronizzazione può essere eliminata se si dispone di una successiva chiamata API di blocco come questa:

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

in questo caso la cudaMemcpychiamata può restituire sia gli errori che si sono verificati durante l'esecuzione del kernel, sia quelli della copia di memoria stessa. Questo può creare confusione per i principianti e consiglierei di utilizzare la sincronizzazione esplicita dopo il lancio del kernel durante il debug per rendere più facile capire dove potrebbero sorgere problemi.

Si noti che quando si utilizza il parallelismo dinamico CUDA , una metodologia molto simile può e deve essere applicata a qualsiasi utilizzo dell'API di runtime CUDA nei kernel del dispositivo, nonché dopo il lancio di qualsiasi kernel del dispositivo:

#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);
   }
}

8
@harrism: non credo. Community Wiki è pensato per domande o risposte che vengono modificate frequentemente. Questo non è uno di quelli
talonmies

1
non dovremmo aggiungere anche cudaDeviceReset()prima di uscire? E una clausola per la deallocazione della memoria?
Aurelio

2
@talonmies: per le chiamate di runtime Async CUDA, come cudaMemsetAsync e cudaMemcpyAsync, richiede anche la sincronizzazione del dispositivo gpu e del thread host tramite chiamata a gpuErrchk (cudaDeviceSynchronize ())?
nurabha

2
Si noti che la sincronizzazione esplicita dopo il lancio del kernel non è sbagliata ma può alterare gravemente le prestazioni di esecuzione e la semantica di interleaving. Se stai usando l'interleaving, la sincronizzazione esplicita per il debug potrebbe nascondere un'intera classe di bug che potrebbero essere difficili da rintracciare nella build di rilascio.
masterxilo

C'è un modo per ottenere errori più specifici per le esecuzioni del kernel? Tutti gli errori che ricevo mi danno solo il numero di riga dal codice host, non dal kernel.
Azmisov

70

La risposta di talonmies sopra è un ottimo modo per interrompere un'applicazione in assertmodo stile.

Occasionalmente potremmo voler segnalare e ripristinare una condizione di errore in un contesto C ++ come parte di un'applicazione più grande.

Ecco un modo ragionevolmente conciso per farlo generando un'eccezione C ++ derivata std::runtime_errordall'uso thrust::system_error:

#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);
  }
}

Questo incorporerà il nome del file, il numero di riga e una descrizione in lingua inglese cudaError_tdel .what()membro dell'eccezione generata :

#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;
}

Il risultato:

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

Un cliente di some_functionpuò distinguere gli errori CUDA da altri tipi di errori, se lo desidera:

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;
}

Poiché thrust::system_errorè a std::runtime_error, in alternativa possiamo gestirlo allo stesso modo di un'ampia classe di errori se non richiediamo la precisione dell'esempio precedente:

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;
}

1
Le testate di spinta sembrano essere state riorganizzate. <thrust/system/cuda_error.h>è ora efficace <thrust/system/cuda/error.h>.
chappjc

Jared, penso che la mia libreria wrapper includa la tua soluzione suggerita, per lo più, ed è abbastanza leggera da essere probabilmente sostitutiva. (Vedi la mia risposta)
einpoklum

27

Il metodo C ++ - canonico: non controllare gli errori ... usa i collegamenti C ++ che generano eccezioni.

Ero irritato da questo problema; e avevo una soluzione con funzione macro-wrapper proprio come nelle risposte di Talonmies e Jared, ma, onestamente? Rende l'utilizzo dell'API Runtime CUDA ancora più brutto e simile a C.

Quindi mi sono avvicinato a questo in un modo diverso e più fondamentale. Per un esempio del risultato, ecco parte dell'esempio CUDA vectorAdd, con il controllo completo degli errori di ogni chiamata API di runtime:

// (... 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...)

Ancora una volta: tutti i potenziali errori vengono controllati e un'eccezione se si è verificato un errore (avvertenza: se il kernel ha causato qualche errore dopo l' avvio, verrà rilevato dopo il tentativo di copiare il risultato, non prima; per assicurarti che il kernel abbia avuto successo dovresti è necessario verificare la presenza di errori tra l'avvio e la copia con un cuda::outstanding_error::ensure_none()comando).

Il codice sopra usa my

Wrapper Thin Modern-C ++ per la libreria API Runtime CUDA (Github)

Tieni presente che le eccezioni contengono sia una spiegazione della stringa che il codice di stato dell'API di runtime CUDA dopo la chiamata non riuscita.

Alcuni link su come gli errori CUDA vengono controllati automaticamente con questi wrapper:


10

La soluzione discussa qui ha funzionato bene per me. Questa soluzione utilizza funzioni cuda incorporate ed è molto semplice da implementare.

Il codice pertinente è copiato di seguito:

#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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.