cudaGetLastError。哪个内核执行引起了它?

时间:2019-03-06 10:11:38

标签: cuda gpu

我实现了一个管道,其中在特定的流中启动了许多内核。内核排入流中,并在调度程序确定最佳时执行。

在我的代码中,每个内核进入队列后,我通过调用cudaGetLastError来检查是否存在任何错误,根据文档,“它从运行时调用中返回最后一个错误。此调用还可能从先前的异步操作中返回错误代码。发射”。因此,如果仅将内核排入队列,而不执行,则我了解到,仅当内核正确排入队列时才返回错误(参数检查,网格和块大小,共享内存等)。

我的问题是:我排队等待许多不同的内核,而不必等待每个内核执行的完成。现在想像一下,我的一个内核中存在一个错误(例如,称为Kernel1),该错误会导致非法的内存访问(例如)。如果我在将cudaGetLastError入队后立即对其进行检查,则返回值是成功的,因为它已正确入队。因此,我的CPU线程继续运行,并使内核不断进入流中。在某个时候,将执行Kernel1并引发非法内存访问。因此,下次我检查cudaGetLastError时,我将得到cuda错误,但是到那时,CPU线程是代码中的另一点。因此,我知道发生了错误,但是我不知道哪个内核引发了该错误。

一个选项是同步(阻塞CPU线程),直到每个内核的执行完成,然后检查错误代码,但是出于性能原因,这不是一个选择。

问题是,有什么方法可以查询哪个内核引发了cudaGetLastError返回的给定错误代码?如果没有,那么您认为哪种方法最好呢?

3 个答案:

答案 0 :(得分:3)

有一个environment variable CUDA_​LAUNCH_​BLOCKING,您可以使用它序列化内核执行的过程,否则该过程将以异步方式启动内核。这应该允许您通过主机代码中的内部错误检查,或通过诸如cuda-memcheck之类的外部工具,隔离引起错误的内核实例。

答案 1 :(得分:1)

我已经测试了3种不同的选择:

  1. 将CUDA_LAUNCH_BLOCKING环境变量设置为1。这将强制阻塞CPU线程,直到内核执行完成为止。我们可以在每次执行后检查是否有错误捕获到确切的故障点。虽然,这会对性能产生明显影响,但是这可能有助于在生产环境中绑定错误,而不必在客户端进行任何更改。
  2. 分发用标志-lineinfo编译的生产代码,然后使用cuda-memncheck再次运行该代码。这对性能没有影响,我们也不需要在客户端中进行任何更改。虽然,我们必须在稍微不同的环境中执行二进制文件,但在某些情况下,例如运行GPU任务的服务可能难以实现。
  3. 在每次内核调用之后插入一个回调。在userData参数中,包括内核调用的唯一ID,并可能包含有关所用参数的一些信息。它可以直接分布在生产环境中,并始终为我们提供确切的故障点,而我们不需要在客户端进行任何更改。虽然,这种方法对性能的影响是巨大的。显然,回调函数是由驱动程序线程处理的,并且会影响性​​能。我写了一个代码来测试它

    #include <cuda_runtime.h>
    
    #include <vector>
    #include <chrono>
    #include <iostream>
    
    #define BLOC_SIZE       1024
    #define NUM_ELEMENTS    BLOC_SIZE * 32
    #define NUM_ITERATIONS  500
    
    __global__ void KernelCopy(const unsigned int *input, unsigned int *result) {
      unsigned int pos = blockIdx.x * BLOC_SIZE + threadIdx.x;
      result[pos] = input[pos];
    }
    
    void CUDART_CB myStreamCallback(cudaStream_t stream, cudaError_t status, void *data) {
      if (status) {
        std::cout << "Error: " << cudaGetErrorString(status) << "-->";
      }
    }
    
    #define CUDA_CHECK_LAST_ERROR   cudaStreamAddCallback(stream, myStreamCallback, nullptr, 0)
    
    int main() {
      cudaError_t c_ret;
      c_ret = cudaSetDevice(0);
      if (c_ret != cudaSuccess) {
        return -1;
      }
    
      unsigned int *input;
      c_ret = cudaMalloc((void **)&input, NUM_ELEMENTS * sizeof(unsigned int));
      if (c_ret != cudaSuccess) {
        return -1;
      }
    
      std::vector<unsigned int> h_input(NUM_ELEMENTS);
      for (unsigned int i = 0; i < NUM_ELEMENTS; i++) {
        h_input[i] = i;
      }
    
      c_ret = cudaMemcpy(input, h_input.data(), NUM_ELEMENTS * sizeof(unsigned int), cudaMemcpyKind::cudaMemcpyHostToDevice);
      if (c_ret != cudaSuccess) {
        return -1;
      }
    
      unsigned int *result;
      c_ret = cudaMalloc((void **)&result, NUM_ELEMENTS * sizeof(unsigned int));
      if (c_ret != cudaSuccess) {
        return -1;
      }
    
      cudaStream_t stream;
      c_ret = cudaStreamCreate(&stream);
      if (c_ret != cudaSuccess) {
        return -1;
      }
    
      std::chrono::steady_clock::time_point start;
      std::chrono::steady_clock::time_point end;
    
      start = std::chrono::steady_clock::now();
      for (unsigned int i = 0; i < 500; i++) {
        dim3 grid(NUM_ELEMENTS / BLOC_SIZE);
        KernelCopy <<< grid, BLOC_SIZE, 0, stream >>> (input, result);
        CUDA_CHECK_LAST_ERROR;
      }
      cudaStreamSynchronize(stream);
      end = std::chrono::steady_clock::now();
      std::cout << "With callback took (ms): " << std::chrono::duration<float, std::milli>(end - start).count() << '\n';
    
      start = std::chrono::steady_clock::now();
      for (unsigned int i = 0; i < 500; i++) {
        dim3 grid(NUM_ELEMENTS / BLOC_SIZE);
        KernelCopy <<< grid, BLOC_SIZE, 0, stream >>> (input, result);
        c_ret = cudaGetLastError();
        if (c_ret) {
          std::cout << "Error: " << cudaGetErrorString(c_ret) << "-->";
        }
      }
      cudaStreamSynchronize(stream);
      end = std::chrono::steady_clock::now();
      std::cout << "Without callback took (ms): " << std::chrono::duration<float, std::milli>(end - start).count() << '\n';
    
      c_ret = cudaStreamDestroy(stream);
      if (c_ret != cudaSuccess) {
        return -1;
      }
      c_ret = cudaFree(result);
      if (c_ret != cudaSuccess) {
        return -1;
      }
      c_ret = cudaFree(input);
      if (c_ret != cudaSuccess) {
        return -1;
      }
    
      return 0;
    }
    

输出:

使用回调所需的时间(毫秒):47.8729

未花费回调(毫秒):1.9317

(CUDA 9.2,Windows 10,Visual Studio 2015,Nvidia Tesla P4)

对我来说,在生产环境中,唯一有效的方法是数字2。

答案 2 :(得分:-3)

如果正确编写了内核和初始化(应该是代码的最终状态),则无需进行cudaGetLastError检查。

但是,在开发阶段,情况并非如此。在三种情况下,可能会导致内核失败:编码错误,或者正在处理的内存分配不正确,或者由于某种原因设备不可用。对于最后两个,错误不在内核中,您应该能够编写代码,以检查在调用内核之前是否存在这种情况。我建议始终添加这些错误检查器,因为大多数情况下,您将需要进行某种同步(例如,等待memcpy或等待获得设备信息(这是一般的同步))。

如果可以使用错误检查内存(和其他相关调用),则由于某些开发错误,内核调用将失败。但是,最终代码应该没有错误(或者就是我们想要的)。

因此,我的建议是:在每个内核之前同步CPU。这是一个开发工具,在此阶段您不必担心性能。修复您的内核,一旦知道它们不会失败,请删除同步并在最后(或现在有的地方)保留错误检查。由于最终的代码不会有由错误引起的错误,因此无需检查它们。