En regardant à travers les réponses et les commentaires sur les questions CUDA, et dans le = CUDA tag wiki , je vois qu'il est souvent suggéré que le statut de retour de chaque appel d'API devrait être vérifié pour les erreurs. La documentation de l'API contient des fonctions telles que cudaGetLastError
, cudaPeekAtLastError
et cudaGetErrorString
, mais quel est le meilleur moyen de les assembler pour détecter et signaler les erreurs de manière fiable sans nécessiter beaucoup de code supplémentaire?
Le meilleur moyen de vérifier les erreurs dans le code de l'API d'exécution est probablement de définir une fonction de gestionnaire de style d'assertion et une macro wrapper similaires à celles-ci:
#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);
}
}
Vous pouvez ensuite encapsuler chaque appel d'API avec la macro gpuErrchk
, qui traitera l'état de retour de l'appel d'API à encapsuler, par exemple:
gpuErrchk( cudaMalloc(&a_d, size*sizeof(int)) );
S'il y a une erreur dans un appel, un message textuel décrivant l'erreur, ainsi que le fichier et la ligne de votre code où l'erreur s'est produite seront envoyés à stderr
et l'application se fermera. Vous pourriez éventuellement modifier gpuAssert
pour déclencher une exception plutôt que d'appeler exit()
dans une application plus sophistiquée, le cas échéant.
Une autre question connexe est de savoir comment vérifier les erreurs dans les lancements du noyau, qui ne peuvent pas être directement encapsulées dans un appel de macro comme les appels d'API d'exécution standard. Pour les noyaux, quelque chose comme ceci:
kernel<<<1,1>>>(a);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );
vérifie d'abord l'argument de lancement non valide, puis force l'hôte à attendre que le noyau s'arrête et recherche une erreur d'exécution. La synchronisation peut être éliminée si vous avez un autre appel d'API de blocage comme ceci:
kernel<<<1,1>>>(a_d);
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaMemcpy(a_h, a_d, size * sizeof(int), cudaMemcpyDeviceToHost) );
dans ce cas, l'appel cudaMemcpy
peut renvoyer des erreurs survenues lors de l'exécution du noyau ou celles provenant de la copie de mémoire elle-même. Cela peut être déroutant pour le débutant, et je vous recommande d'utiliser une synchronisation explicite après le lancement du noyau lors du débogage afin de faciliter la compréhension de l'endroit où des problèmes peuvent survenir.
Notez qu'en cas d'utilisation de CUDA Dynamic Parallelism , une méthodologie très similaire peut et devrait être appliquée à toute utilisation de l'API d'exécution CUDA dans les noyaux de périphériques, ainsi qu'après le lancement de tout noyau de périphérique:
#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);
}
}
la réponse de Talonmies ci-dessus est une excellente façon d’abandonner une application dans le style assert
-.
De temps en temps, nous pouvons souhaiter signaler et récupérer une condition d'erreur dans un contexte C++ dans le cadre d'une application plus grande.
Voici un moyen raisonnablement sommaire de le faire en lançant une exception C++ dérivée de std::runtime_error
à l'aide de 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);
}
}
Ceci incorporera le nom de fichier, le numéro de ligne et une description en anglais du cudaError_t
dans le membre .what()
de l'exception levée:
#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;
}
Le résultat:
$ nvcc exception.cu -run
CUDA error after cudaSetDevice: exception.cu(23): invalid device ordinal
Un client de some_function
peut, s'il le souhaite, distinguer les erreurs CUDA d'autres types d'erreur:
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;
}
Comme thrust::system_error
est un std::runtime_error
, nous pouvons également le traiter de la même manière qu'une vaste classe d'erreurs si nous n'exigeons pas la précision de l'exemple précédent:
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;
}
J'avais l'habitude d'être irrité par ce problème; et j'avais l'habitude d'avoir une solution macro-cum-wrapper-function comme dans les réponses de Talonmies et Jared, mais honnêtement? Cela rend l’utilisation de l’API CUDA Runtime encore plus laide et semblable à C.
J'ai donc abordé cette question d'une manière différente et plus fondamentale. Pour obtenir un exemple du résultat, voici une partie de CUDA vectorAdd
exemple - avec vérification d'erreur complète de chaque appel d'API d'exécution:
// (... 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...)
Encore une fois - toutes les erreurs potentielles sont vérifiées et signalées via une exception levée. Ce code utilise mon
wrappers Thin Modern-C++ pour la bibliothèque d'API CUDA Runtime (Github)
Notez que les exceptions portent à la fois une explication de chaîne et le code d'état de l'API d'exécution CUDA après l'appel ayant échoué.
Quelques liens sur la manière dont les erreurs CUDA sont vérifiées de manière automatique avec ces wrappers:
La solution discutée ici a bien fonctionné pour moi. Cette solution utilise des fonctions cuda intégrées et est très simple à mettre en œuvre.
Le code correspondant est copié ci-dessous:
#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;
}