一、调试的最基本的方法
在前面的CUDA调试中给出了很多种方法,重点介绍了使用IDE相关的调试。但实际在某些情况下,不少开发者还是更青睐直接打印数据结果。特别是可能无法使用IDE的场景下,这种打印可能起着重要的作用。
那么CUDA编程中的打印数据和主机应用中的普通编程打印有什么不一样呢?本文将对其进行重点的分析。
二、CUDA中的printf
在CUDA编程的系列中,前面的环境安装中,给出一个最简单的例子。通过不同的函数来区分执行的过程是在CPU还是在GPU上。可以非常清楚的让普通开发者看清楚代码是否被CUDA框架调用进入到了设备端进行运行。
但这个例程太简单了,简单到极有可能让大家产生误会。就一如人们批评某位C语言的老师误导开发者一样,事实确实也是如此。
首先CUDA中的printf并不完全兼容传统的C语言中的printf,或者说它只是C语言中printf的一个子集(对格式支持和参数数量都有限制,如打印__half类型,必须先转float)。它在引入相关的头文件后,可以直接在内核函数中调用。其次,printf函数的缓冲区是一个有限大小的环形缓冲区(1M左右,当然也可以通过接口对其大小进行设置)。再次,CUDA编程的并行性和异步性,导致了其输出的异步性和缓冲区的刷新机制有所不同,需要使用同步机制进行处理(如cudaDeviceSynchronize()等同步机制)并控制日志输出时不被覆盖。而并行性也可能导致其输出的顺序的非确定性。最后,printf函数应用成本并不低,它可能导致性能的下降。这意味着真正部署的代码中尽量去除这些打印的相关代码。
三、如何正确应用
而在真正的应用中,一般很少直接使用printf函数进行日志输出。更多的是使用宏进行封装,然后再对宏进行应用。在调试和发布间只通过宏的处理即可将相关的调试信息全部去除。一般如下面的情况:
c
#define CUDA_DEBUG 1
#if CUDA_DEBUG
#define DEBUG_PRINT(fmt, args...) printf("DEBUG: " fmt, ##args)
#else
#define DEBUG_PRINT(fmt, args...)
#endif
__global__ void kerfunc() {
int tid = threadIdx.x;
DEBUG_PRINT("cur Thread id %d \n", tid);
}
也可以进一步的封装:
c
#include <stdio.h>
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA Error: %s (%s:%d)\n", \
cudaGetErrorString(err), __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while(0)
在一些开源的代码中经常可以看到上类似的代码,而且它们看上去和在CPU中打印没有本质的不同,只是运行的位置不同罢了。
四、例程
基础的应用往往能够解决一些细节的问题,所以对printf还是得专门搞个例子来看看:
c
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA Error: %s (%s:%d)\n", \
cudaGetErrorString(err), __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while(0)
__global__ void printfTest()
{
int tid = threadIdx.x + blockIdx.x * blockDim.x;
printf("[Block %d, Thread %d] => Global thread ID: %d\n",
blockIdx.x, threadIdx.x, tid);
}
int main()
{
CUDA_CHECK(cudaDeviceSetLimit(cudaLimitPrintfFifoSize, 16 * 1024 * 1024)) ;
int blocks = 2;
int threads = 4;
printfTest << <blocks, threads >> > ();
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaDeviceSynchronize());
printf("\nkernel printf output end.\n");
return 0;
}
例子确实不复杂,但基本上还是比较清楚的描述了printf的应用方法和相关的初步封装。
五、总结
在前面的调试中其实是忽略了对printf的应用分析。但此函数还是相当重要的的。如果应用得当,它可以起到关键的作用。让开发者能够很快的找到关心的细节结果。