我实现了一个管道,其中在特定的流中启动了许多内核。内核排入流中,并在调度程序确定最佳时执行。
在我的代码中,每个内核进入队列后,我通过调用cudaGetLastError来检查是否存在任何错误,根据文档,“它从运行时调用中返回最后一个错误。此调用还可能从先前的异步操作中返回错误代码。发射”。因此,如果仅将内核排入队列,而不执行,则我了解到,仅当内核正确排入队列时才返回错误(参数检查,网格和块大小,共享内存等)。
我的问题是:我排队等待许多不同的内核,而不必等待每个内核执行的完成。现在想像一下,我的一个内核中存在一个错误(例如,称为Kernel1),该错误会导致非法的内存访问(例如)。如果我在将cudaGetLastError入队后立即对其进行检查,则返回值是成功的,因为它已正确入队。因此,我的CPU线程继续运行,并使内核不断进入流中。在某个时候,将执行Kernel1并引发非法内存访问。因此,下次我检查cudaGetLastError时,我将得到cuda错误,但是到那时,CPU线程是代码中的另一点。因此,我知道发生了错误,但是我不知道哪个内核引发了该错误。
一个选项是同步(阻塞CPU线程),直到每个内核的执行完成,然后检查错误代码,但是出于性能原因,这不是一个选择。
问题是,有什么方法可以查询哪个内核引发了cudaGetLastError返回的给定错误代码?如果没有,那么您认为哪种方法最好呢?
答案 0 :(得分:3)
有一个environment variable CUDA_LAUNCH_BLOCKING
,您可以使用它序列化内核执行的过程,否则该过程将以异步方式启动内核。这应该允许您通过主机代码中的内部错误检查,或通过诸如cuda-memcheck
之类的外部工具,隔离引起错误的内核实例。
答案 1 :(得分:1)
我已经测试了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。这是一个开发工具,在此阶段您不必担心性能。修复您的内核,一旦知道它们不会失败,请删除同步并在最后(或现在有的地方)保留错误检查。由于最终的代码不会有由错误引起的错误,因此无需检查它们。