CUDA C/C++ 优化数据传输的效率、流的使用
liebian365 2024-11-16 23:10 23 浏览 0 评论
在本文中,我们讨论如何将数据传输与主机上的计算,设备上的计算以及在某些情况下主机与设备之间的其他数据传输重叠。实现数据传输和其他操作之间的重叠需要使用CUDA流,因此首先让我们了解流。
1、CUDA 流
CUDA中的流是按照主机代码发出的顺序在设备上执行的一系列操作。 虽然保证流中的操作按规定的顺序执行,但是可以交错不同流中的操作,并且在可能的情况下,它们甚至可以同时运行。默认流与其他流不同,因为它是关于设备上的操作的同步流:在设备上的任何流中的所有先前发出的操作完成之前,默认流中的任何操作都不会开始,并且在默认流中的操作必须在任何其他操作(在设备上的任何流中)开始之前完成。
2、默认流
CUDA中的所有设备操作(内核和数据传输)都在流中运行。 如果未指定任何流,则使用默认流(也称为“空流”)。请注意,2015年发布的CUDA 7引入了一个新选项,可以在每个主机线程中使用单独的默认流,并将每个线程默认流视为常规流(即,它们不与其他流中的操作同步)。
让我们看一些使用默认流的简单代码示例,并从主机和设备的角度讨论操作是如何进行的。
cudaMemcpy(d_a, a, numBytes, cudaMemcpyHostToDevice);
increment<<<1,N>>>(d_a);
cudaMemcpy(a, d_a, numBytes, cudaMemcpyDeviceToHost);
在上面的代码中,从设备的角度来看,所有这三个操作都被发布到相同的(默认)流中,并将按照它们被发布的顺序执行。
从主机的角度来看,隐式数据传输是阻塞传输或同步传输,而内核启动是异步的。由于第一行上的主机到设备数据传输是同步的,因此在主机到设备的传输完成之前,CPU线程不会到达第二行上的内核调用。一旦内核被发出,CPU线程就移动到第三行,但是由于设备端的执行顺序,该行上的传输无法开始。
从主机的角度来看,内核启动的异步行为使设备和主机之间的并行变得非常简单。 我们可以修改代码以添加一些独立的CPU计算,如下所示。
cudaMemcpy(d_a, a, numBytes, cudaMemcpyHostToDevice);
increment<<<1,N>>>(d_a);
myCpuFunction(b);
cudaMemcpy(a, d_a, numBytes, cudaMemcpyDeviceToHost)
在上面的代码中,在设备上启动 increment() 内核后,CPU线程立即执行myCpuFunction() ,将其在CPU上的执行与GPU上的内核执行重叠。不管是主机函数还是设备内核先完成,都不会影响后续的设备到主机的传输,只有在内核完成后才会开始数据传输操作。
从设备的角度来看,与前面的示例没有任何变化,设备完全不知道myCpuFunction() 。
3、非默认流
CUDA C / C ++中的非默认流在主机代码中声明,创建和销毁,示例如下:
cudaStream_t stream1;
cudaError_t result;
result = cudaStreamCreate(&stream1);
result = cudaStreamDestroy(stream1);
要将数据传输到非默认流,我们使用 cudaMemcpyAsync() 函数,该函数 cudaMemcpy() 函数,但是将流标识符作为第五个参数。
result = cudaMemcpyAsync(d_a, a, N, cudaMemcpyHostToDevice, stream1);
cudaMemcpyAsync() 函数在主机上是非阻塞的,因此控制在发出传输后立即返回到主机线程。 此函数有 cudaMemcpy2DAsync() 和 cudaMemcpy3DAsync() 变体,可以在指定的流中异步传输2D和3D数据。
要将内核发布给非默认流,我们将流标识符指定为第四个参数(第三个参数分配共享设备内存,现在使用0)。
increment<<<1,N,0,stream1>>>(d_a);
4、与流同步
由于非默认流中的所有操作对于主机代码都是非阻塞的,因此,可以将主机代码与流中的操作同步的情况下运行。
有几种方法可以做到这一点。其中一种方法是使用 cudaDeviceSynchronize() ,它会阻止主机代码,直到设备上以前发出的所有操作都完成。在大多数情况下,这种方法会因为整个设备和主机线程的暂停而真正影响性能。
CUDA流API具有多种不严格的方法来将主机与流同步。
函数 cudaStreamSynchronize(stream) 可用于阻止主机线程,直到指定流中的所有先前发出的操作完成为止。
函数 cudaStreamQuery(stream) 测试是否已完成向指定流发出的所有操作,而不会阻止主机执行。
函数 cudaEventSynchronize(event) 和 cudaEventQuery(event) 的行为与流的对应函数相似,不同之处在于它们的结果基于是否已记录指定的事件而不是指定的流是否空闲。
还可以使用 cudaStreamWaitEvent(event) 对特定事件在单个流中进行同步操作(即使该事件记录在其他流中或在其他设备上)。
5、内核执行与数据传输之间的重叠
前面,我们演示了如何将默认流中的内核执行与主机上的代码执行重叠。 但是,本文的主要目的是向您展示如何将内核执行与数据传输重叠。 要实现这一点,我们需要达到以下几个要求:
(1)该设备必须能够“同时复制和执行”。 可以从 cudaDeviceProp 结构的 deviceOverlap 字段中查询,也可以从 CUDA SDK / Toolkit 附带的 deviceQuery 示例的输出中查询。 几乎所有具有计算功能1.1及更高版本的设备都具有此功能。
(2)内核执行和要重叠的数据传输都必须发生在不同的非默认流中。
(3)数据传输中涉及的主机内存必须是固定内存(pinned memory)。
因此,让我们修改上面的简单主机代码,使用多个流,看看是否可以实现任何重叠。在修改后的代码中,我们将大小为N的数组拆分为streamSize元素的块。由于内核在所有元素上独立运行,因此每个块都可以独立处理。 使用的(非默认)流数为 nStreams = N / streamSize。
有多种方法可以实现数据的区域分解和处理;一种方法是遍历数组每个块的所有操作,如本示例代码所示。
for (int i = 0; i < nStreams; ++i) {
int offset = i * streamSize;
cudaMemcpyAsync(&d_a[offset], &a[offset], streamBytes, cudaMemcpyHostToDevice, stream[i]);
kernel<<< streamSize / blockSize, blockSize, 0, stream[i] >>>(d_a, offset);
cudaMemcpyAsync(&a[offset], &d_a[offset], streamBytes, cudaMemcpyDeviceToHost, stream[i]);
}
另一种方法是将类似的操作批处理在一起,首先发出所有主机到设备的传输,然后是所有内核启动,然后是所有设备到主机的传输,如下面的代码所示。
for (int i = 0; i < nStreams; ++i) {
int offset = i * streamSize;
cudaMemcpyAsync(&d_a[offset], &a[offset], streamBytes, cudaMemcpyHostToDevice,, stream[i]);
}
for (int i = 0; i < nStreams; ++i) {
int offset = i * streamSize;
kernel<<<streamSize/blockSize, blockSize, 0, stream[i]>>>(d_a, offset);
}
for (int i = 0; i < nStreams; ++i) {
int offset = i * streamSize;
cudaMemcpyAsync(&a[offset], &d_a[offset], streamBytes, cudaMemcpyDeviceToHost, stream[i]);
}
上面显示的两种异步方法都能产生正确的结果,并且在两种情况下,依存操作都按照需要执行的顺序发给同一流。 但是,这两种方法的执行情况会有所不同,具体取决于所使用GPU的特定生成时间。
在GeForce GTX 1060上,计算能力为6.1,运行测试代码得出以下结:
Device : GeForce GTX 1060
Time for sequential transfer and execute (ms): 13.192288
max error: 1.192093e-07
Time for asynchronous V1 transfer and execute (ms): 5.993408
max error: 1.192093e-07
Time for asynchronous V2 transfer and execute (ms): 5.891360
max error: 1.192093e-07
完整的代码如下:
官方博客给的示例运行结果如下:
?这里第一次输出的是顺序传输和使用阻塞传输的内核执行,我们将其用作异步加速比较的基准。
为什么两种异步策略在不同的体系结构上表现不同?要了解这些结果,我们需要清楚地了解CUDA设备是如何安排和执行任务的。CUDA设备包含用于各种任务的引擎,这些引擎在发出操作时将操作排入队列。不同引擎中的任务之间的依赖关系得到维护,但在任何引擎中,所有外部依赖关系都会丢失;每个引擎队列中的任务都按其发出的顺序执行。
C1060具有单个复制引擎和单个内核引擎。 下图显示了在C1060上执行示例代码的时间线。
?在示意图中,我们假设主机到设备传输、内核执行和设备到主机传输所需的时间大致相同(选择内核代码是为了实现这一点)。正如顺序内核所期望的,任何操作都没有重叠。
对于我们代码的第一个异步版本,复制引擎中的执行顺序为:H2D流(1),D2H流(1),H2D流(2),D2H流(2),依此类推。
这就是为什么在C1060上使用第一个异步版本时,我们看不到任何加速的原因:任务以防止内核执行和数据传输重叠的顺序发布给复制引擎。
但是,对于版本2,如果所有主机到设备的传输都在任何设备到主机的传输之前发出,则重叠是可能的,执行时间较短。
从我们的示意图中,我们期望异步版本2的执行是顺序版本的8/12,即8.7 ms,这在前面给出的时序结果中得到了证实。
在C2050上,架构上的不同,因此与C1060有所不同。
C2050有两个拷贝引擎,一个用于主机到设备的传输,另一个用于设备到主机的传输,还有一个内核引擎。下图演示了我们的示例在C2050上的执行。
?具有两个复制引擎可以解释为什么异步版本1在C2050上可以实现良好的加速:流[i]中从设备到主机的数据传输不会阻止流[i + 1]中从主机到设备的数据传输 ,就像在C1060上所做的那样,因为C2050的每个复制方向都有一个单独的引擎。
该示意图预测执行时间相对于顺序版本将减少一半,这大致是我们的时序结果所显示的。
但是,在C2050的异步版本2中观察到的性能下降是为什么呢? 这与C2050可以同时运行多个内核的能力有关。
当多个内核在不同(非默认)流中连续发出时,调度程序会尝试启用这些内核的并发执行,因此会延迟通常在每个内核完成(负责启动设备到主机的传输)之后出现的信号,直到所有内核完成。
因此,尽管在异步代码的第2版中主机到设备的传输和内核执行之间存在重叠,但是内核执行和设备到主机的传输之间没有重叠。
该示意图预测异步版本2的总时间为顺序版本的时间的9/12,即7.5 ms,这一点已由我们的计时结果证实。
好消息是,对于具有计算能力3.5(K20系列)的设备,Hyper-Q特性消除了定制启动顺序的需要,因此上述任何一种方法都将起作用。我们将在以后的文章中讨论使用 Kepler 特性,但现在,这里是在 Tesla K20c GPU 上运行示例代码的结果。这两种异步方法在同步代码上实现了相同的加速比。
?6、总结
本篇文章主要介绍流以及如何通过并发执行拷贝和内核来使用它们来覆盖数据传输时间。
相关推荐
- Markdown 常用语法总结(markdown示例)
-
头条不能以代码模式查看,所以分两部分来写:效果、语法。效果和语法部分一一对应,最好自己把语法复制下来保存为.md用md编辑器打开。先看效果:Markdown常用语法注:查阅时在视图中切换为源代码模式...
- CPU眼里的:字符串 vs 数组(字符数组与字符串区别)
-
“它们十分相似,但又非常不同”01提出问题字符串和字符数组,在内存分布上,跟普通数组(例如:int类型的数组)有很高的相似性。但使用字符串的危险系数,却远远高于普通数组。是什么细微的差异导致了二者在使...
- rsync命令详解(rsync命令详解 -X)
-
1.rsync简介rsync是linux系统下的数据镜像备份工具。使用快速增量备份工具RemoteSync可以远程同步,支持本地复制,或者与其他SSH、rsync主机同步。2.rsync特性rsy...
- Linux操作系统安全配置(linux系统的安全配置有哪些方面)
-
一、服务相关命令systemctlenable服务名#开机自启动systemctldisable服务名#禁用开机自启动systemctlstop服务名#停止服务systemctls...
- 一篇文章学会数据备份利器rsync(备份数据语句)
-
阿铭linux近16年的IT从业经验,6年+鹅厂运维经验,6年+创业公司经验,熟悉大厂运维体系,有从零搭建运维体系的实战经验。关注我,学习主流运维技能,让你比别人提升更快,涨薪更多!作为一个系统管理员...
- 成功尝试在NetBSD9.0中安装Mate Desktop环境记录
-
NETBSD系统桌面安裝系統最新的NetBSD9.0:http://cdn.netbsd.org/pub/NetBSD/NetBSD-9.0/images/https://mirrors.tuna.t...
- 使用OpenLDAP集中式认证(openresty集群)
-
1OpenLDAP入门1.1什么是LDAP?1.2我不理解。什么是目录?1.3信息结构是什么样?1.4所以……它可以用来做什么?2OpenLDAP服务器配置2.1.1OLC样式的LDIF...
- 在 Ubuntu 22.04 上安装和配置 VNC 远程桌面
-
环境Ubuntu22.04.2LTSx86_64Step-1安装桌面环境Ubuntu默认使用GNOME桌面环境,但也可以安装其他桌面环境,如Xfce、KDE等。这个可以根据个人喜好选...
- hdfs集群的扩容和缩容(hdfs容量)
-
1、背景当我们的hadoop集群运行了一段时间之后,原有的数据节点的容量已经不能满足我们的存储了,这个时候就需要往集群中增加新的数据节点。此时我们就需要动态的对hdfs集群进行扩容操作(节点服役)。2...
- Zabbix入门操作指南(zabbix怎么使用)
-
上篇:安装与配置一.概述在开始之前,一些概念和定义需要我们提前了解一下(以下内容摘自官方网站)。1.1几个概念架构Zabbix由几个主要的功能组件组成,其职责如下所示。ServerZabbixs...
- 从0开始学习KVM-KVM学习笔记(6)- CentOS远程桌面连接
-
CentOS远程桌面连接CentOS系统上配置远程桌面连接有多种方法,其中最常用的是通过xrdp或vnc来实现。安装xrdr实现CentOS远程桌面安装xrdp安装epel库sudoyu...
- systemd service之:服务配置文件编写(2)
-
接下来会通过示例来描述不同ServiceType值的应用场景。在此之前,强烈建议先阅读前后台进程父子关系和daemon类进程来搞懂进程之间的关系和Daemon类进程的特性。systemdservi...
- Linux项目开发,你必须了解Systemd服务!
-
1.Systemd简介Systemd是什么,以前linux系统启动init机制,由于init一方面对于进程的管理是串行化的,容易出现阻塞情况,另一方面init也仅仅是执行启动脚本,并不能对服务本身...
- Oracle 数据库日常巡检之检查数据库安全性
-
在本节主要检查Oracle数据库的安全性,包含:检查系统安全信息,如系统账户,系统防火墙策略,密码策略等。1.检查系统安全信息系统安全日志文件的目录在/var/log下,主要检查登录成功或失败的用户日...
- 「分享」非常全面的CentOS7系统安全检测和加固脚本
-
CentOS7系统检测和加固脚本脚本来源:https://github.com/xiaoyunjie/Shell_Script主要是为了Linux系统的安全,通过脚本对Linux系统进行一键检测和一键...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)