在本篇文章中,我们围绕数组求和这个例子进行。我们将会介绍内存传输、错误处理和时间统计。
1. 内存传输
此处介绍的内存传输,主要是涉及设备内存相关的。我们可以结合主机上的相关函数进行类比理解。
cudaMalloc 用于在 GPU 内存中分配空间;cudaMemset 用于设置 GPU 内存内容;cudaFree 用于释放 GPU 内存。
C |
CUDA |
malloc |
cudaMalloc |
memset |
cudaMemset |
free |
cudaFree |
内存传输我们使用 cudaMemcpy,涉及主机到设备、设备到主机和设备到设备。
cudaMemcpy 的函数原型为:
- cudaError_t cudaMemcpy(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind);
其中,dst 是目标地址;src 是源地址;count 是要复制的字节数;kind 是要复制的类型,cudaMemcpyHostToHost 主机到主机、cudaMemcpyHostToDevice 主机到设备、cudaMemcpyDeviceToHost 设备到主机、cudaMemcpyDeviceToDevice 设备到设备。
我们看一个最基本的样例。如代码清单 1 所示,我们先在主机上初始化一个随机内容的数组。接着使用 cudaMalloc 申请一块设备内存,然后使用 cudaMemcpy 把初始化的主机内容传输到申请的这块设备内存上。这样就能把这块设备内存传递给核函数使用。最后不要忘记使用 cudaFree 释放设备内存。
执行配置可能会设置的比数组规模大,所以核函数参数中多增加一个 size 参数,用于指示数组规模。
试了一下“越界”访问,也没引起什么“异常”。访问到的数据内容都是 0。
2. 数组求和
我们首先实现 GPU 版本的数组求和。如代码清单 2.1 所示,我们传入三个数组,把前两个数组的各个元素的和,存放到第三个数组中。各元素计算没有依赖,可以直接并行。
计算的再快,如果计算结果是错的,那也没用。所以如代码清单 2.2 所示,我们也实现 CPU 版本的数组求和。并实现数组比较函数。
代码清单 2.3 是具体的求和以及核对流程。如第 7 至 15 行所示,我们初始化两个随机数组,并使用 CPU 计算它们的数组和。
第 18 至 25 行,我们初始化核函数的输入参数。我们把两个主机数组内容复制到设备内存上。
第 28 至 33 行,我们设置执行配置,运行核函数,并等待执行完毕。
第 35 行,我们把设备内存上计算得到的结果,复制到主机内存上,用于后续核对。
第 38 行,我们核对 CPU 计算的结果与 GPU 计算的结果,比较是否相同。
注意这边的 CUDA 内存相关操作单位都是字节。不要写错了。
3. 错误处理
CUDA API 的调用可能会因为各种原因失败,比如内存分配失败、非法操作、设备不支持等。所以生产环境上,规范的做法是检查 CUDA API 调用的返回值。
和平时的 CPU 编程要求是一样的。比如,生产环境上也要检查 malloc 是否分配成功。
CUDA 错误码通过 cudaError_t 枚举类型返回。可以使用 cudaGetErrorString 函数,将错误码转成描述性字符串,方便调试。
比如,在代码清单 3.1 中,我们对 cudaMalloc 进行了错误处理。
如果仅是错误打印逻辑,并且嫌代码太冗长的话,我们可以使用代码清单 3.2 中的宏,用它“包裹”调用的 CUDA API 函数。
4. 时间统计
最后,我们对代码中的关键执行过程进行时间统计。以此感受一下 CPU 和 GPU 的执行时间差异。
如代码清单 4 所示,我们对以下流程进行统计:
1. CPU 数组求和时间。对应第 16 行的过程。
2. 主机内存传输到设备内存的时间。对应第 27 至 28 行的过程。
3. GPU 数组求和时间。对应第 37 至 38 行的过程。
4. 设备内存传输到主机内存的时间。对应第 42 行的过程。
GPU 编程要算上内存传输的额外消耗,所以 2 + 3 + 4 就是 GPU 进行数组求和的总时间。
运行程序,打印的内容如下,可以看到此时 CPU 的执行速度是快于 GPU 的。
- CPU sum time: 123 us
- GPU kernel execution time sum time: 4147 us
- Mem transfer host to device: 287 us
- Mem transfer device to host: 48 us
- Total GPU time: 4482 us
应该是现在的操作简单,且数据规模不大。我们把数组的大小扩大 100 倍,可以发现此时 GPU 就比 CPU 快了。同时可以看到 GPU 执行的时间基本稳定,主要是数据传输的耗时变大。
- CPU sum time: 10069 us
- GPU kernel execution time sum time: 4399 us
- Mem transfer host to device: 1591 us
- Mem transfer device to host: 737 us
- Total GPU time: 6727 us