CUDA编程笔记004. 测量kernel耗时
1、使用cpu时钟测量kernel耗时
#include <sys/time.h>
double CpuSeconds() { struct timeval tp; gettimeofday(&tp, NULL); return ((double)tp.tv_sec + (double)tp.tv_usec * 1e-6); }
|
参考:
2、检查cuda接口返回值
cuda的api会统一返回cudaError_t
错误码,可以通过cudaGetErrorString()
获取错误码对应的错误字符串。
将校验错误码的路径归纳为下面的宏,方便使用。
#define CHECK(call) \ do { \ const cudaError_t error = call; \ if (error != cudaSuccess) { \ printf("Error: %s: %d, ", __FILE__, __LINE__); \ printf("code: %d, reason: %s\n", error, cudaGetErrorString(error)); \ exit(1); \ } \ } while(0)
|
参考:
3、cuda kernel实现
__global__ void kernelAdd(float *d_a, float *d_b, float *d_c, int n_elem) { int id = threadIdx.x + blockIdx.x * blockDim.x;
if (id < n_elem) { d_c[id] = d_a[id] + d_b[id]; } }
|
4、调用cuda kernel
dim3 block(std::min(n_elem, max_thread_per_block)); dim3 grid((n_elem + block.x - 1) / block.x);
auto start_time = CpuSeconds();
kernelAdd<<<grid, block>>>(d_a, d_b, d_c, n_elem);
cudaDeviceSynchronize();
printf("cuda add elapsed: %.6lfs\n", CpuSeconds() - start_time);
|
根据实际计算量划分block和grid的示意图如下:
可以看到,虽然cuda生成了grid * block个线程,但最后一个block中的线程不一定会被全部用掉。
如何获取max_thread_per_block?
在host侧设置的gridDims和blockDims,受具体的硬件限制,在不同的GPU的上限是不同的。cuda提供了接口获取当前GPU硬件的上限信息。
代码如下:
cudaDeviceProp device_prop; int dev = 0; CHECK(cudaGetDeviceProperties(&device_prop, dev)); printf("Using device %d: %s, maxThreadsPerBlock: %d.\n\n", dev, device_prop.name, device_prop.maxThreadsPerBlock); CHECK(cudaSetDevice(dev));
|
参考:
5、使用nvprof测量耗时
nvprof加上所需执行的应用命令,即可执行profiling。本demo效果如下:
$ nvprof ./cuda_vector_add_timing CUDA Demo: add two vectors. ==191428== NVPROF is profiling process 191428, command: ./cuda_vector_add_timing Using device 0: NVIDIA GeForce GTX 1050 Ti, maxThreadsPerBlock: 1024.
testing n_elem: 16777216 ... cpu add elapsed: 0.069825s cuda block:(1024, 1, 1), grid:(16384, 1, 1) cuda add elapsed: 0.002238s cpu result : [1.735893, 0.446092, 1.168937, 1.654771, 1.396988, 1.141601, 0.817212, 1.561706, 0.467451, 1.386464, ...] cuda result: [1.735893, 0.446092, 1.168937, 1.654771, 1.396988, 1.141601, 0.817212, 1.561706, 0.467451, 1.386464, ...] cuda equals to cpu? yes.
==191428== Profiling application: ./cuda_vector_add_timing ==191428== Profiling result: Type Time(%) Time Calls Avg Min Max Name GPU activities: 67.72% 39.002ms 1 39.002ms 39.002ms 39.002ms [CUDA memcpy DtoH] 28.77% 16.571ms 2 8.2853ms 8.0925ms 8.4781ms [CUDA memcpy HtoD] 3.51% 2.0214ms 1 2.0214ms 2.0214ms 2.0214ms kernelAdd(float*, float*, float*, int)
|
计算两个16M数组的加法,CPU耗时70ms,GPU耗时2.23ms,是CPU的1/32。通过profiling结果可以看到,kernelAdd耗时2.02ms,比我们在host侧自己统计的耗时要短一点。这是因为nvprof能够严格测出device侧kernel执行耗时,而不必加上额外的host-device通信操作,更加精确。另外,host-device单次内存拷贝时间是8ms,是计算耗时的4倍。
参考:CUDA 专业提示:nvprof 是你便捷的通用 GPU 剖析器
小结
以上通过CPU时间戳和nvprof两种方式测量了kernel执行耗时。
本文示例完整代码请见:cuda_vector_add_timing.cu。
参考
[1] “Professional Cuda C Programming” Chapter 2.