《CUDA编程》11.CUDA流

本章将介绍CUDA流

CUDA程序的并行层次主要有两个:一个是核函数内部的并行,一个是核函数外部的并行,核函数外部的并行主要指:

  1. 核函数计算与数据传输之间的并行
  2. 主机计算与数据传输之间的并行
  3. 不同数据传输之间的并行
  4. 核函数计算与主机计算之间的并行
  5. 不同核函数之间的并行

为了实现上述所说的并行,需要合理的使用CUDA流

1 CUDA流

定义:指由主机发出的在一个设备中执行的CUDA操作序列

一个CUDA流中各个操作的次序是由主机控制,并按照主机发布的次序执行。

来自两个不同CUDA流中的操作不一定按照某个次序执行,有可能交错或者并发执行

①默认流 / 空流: 默认流是指当没有显式指定流时,CUDA的API调用所使用的流。
②非默认流 / 非空流: 在CUDA编程中显式创建并使用的流

一个CUDA流由类型cudaStream_t的变量表示,可由如下CUDA运行时API产生:

  • cudaError_t cudaStreamCreate(cudaStream_t*);

该函数的输入参数是 cudaStream_t 类型的指针,返回一个错误代号

CUDA 流可由如下 CUDA 运行时 API 函数销毁:

  • cudaError_t cudaStreamDestroy(cudaStream_t);

该函数的输入参数是 cudaStream_t 类型的变量,返回一个错误代号。

下面展示一个CUDA流的定义、产生和销毁:

cudaStream_t stream_1;
cudaStreamCreate(&stream_1);
cudaStreamDestroy(stream_1);

为了实现不同流之间的并发,主机在向某个 CUDA 流中发布一系列命令之后必须马上获得程序的控制权,不用等待该 CUDA 流中的命令在设备中执行完毕。为了检查一个 CUDA 流中的所有操作是否都在设备中执行完毕,CUDA 运行时 API 提供了如下两个函数:

  • cudaError_t cudaStreamSynchronize(cudaStream_t stream);强制阻塞主机,直到 CUDA 流 stream 中的所有操作都执 行完毕。
  • cudaError_t cudaStreamQuery(cudaStream_t stream);不会阻塞主机,只是检查 CUDA 流 stream 中的所有操作 是否都执行完毕

若是成功返回 cudaSuccess,否则返回 cudaErrorNotReady

2 在默认流中重叠主机和设备计算

虽然在一个默认的CUDA流中的所有操作都是顺序执行的,但可以通过一些方法在默认流中重叠主机和设备的计算,我们通过下面遗一串代码来理解:

cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice);
cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice); 
sum<<<grid_size, block_size>>>(d_x, d_y, d_z, N); 
cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost);

以上的4中操作将在默认的CUDA流中,按照顺序依次执行,即:

  1. 主机发出命令执行cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice);
  2. 等命令1执行完毕之后,执行命令cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice);
  3. 等命令2执行完毕后,主机发出命令执行sum<<<grid_size, block_size>>>(d_x, d_y, d_z, N); 注意:在发出调用核函数的命令之后,主机不会等待该命令执行完毕,因为此时是设备在执行操作,所以主机紧接着会发出下一个命令
  4. 然而,cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost);不会被立即执行,因为这是默认流中的 CUDA 操作,必须等待前一个 CUDA 操 作(即核函数的调用)执行完毕才会开始执行。

如果我们能让主机调用核函数之后,同时去进行一些计算,就能提升主机的利用率,这也就是在默认流中重叠主机和设备计算

下面代码展示了一种做法:

#include <cuda_runtime.h>
#include <iostream>#define N 1024 * 1024
#define M N * sizeof(float)
#define THREADS_PER_BLOCK 256// CUDA 核函数:设备上执行数组相加
__global__ void sum(const float* x, const float* y, float* z, int n) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < n) {z[idx] = x[idx] + y[idx];}
}int main() {// 定义主机和设备内存float* h_x, * h_y, * h_z;float* d_x, * d_y, * d_z;cudaMallocHost((void**)&h_x, M); // 主机内存:分页锁定内存cudaMallocHost((void**)&h_y, M);cudaMallocHost((void**)&h_z, M);cudaMalloc((void**)&d_x, M);     // 设备内存cudaMalloc((void**)&d_y, M);cudaMalloc((void**)&d_z, M);// 初始化主机数据for (int i = 0; i < N; ++i) {h_x[i] = static_cast<float>(i);h_y[i] = static_cast<float>(i * 2);}// 异步将数据从主机传输到设备cudaMemcpyAsync(d_x, h_x, M, cudaMemcpyHostToDevice, cudaStreamDefault);cudaMemcpyAsync(d_y, h_y, M, cudaMemcpyHostToDevice, cudaStreamDefault);// 启动核函数计算int grid_size = (N + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK;sum << <grid_size, THREADS_PER_BLOCK >> > (d_x, d_y, d_z, N);// 主机端进行其他计算(在等待核函数完成的同时)float host_computation_result = 0.0f;for (int i = 0; i < N; i += 100) {host_computation_result += h_x[i] * h_y[i]; // 示例主机计算}std::cout << "Host computation result: " << host_computation_result << std::endl;// 异步从设备传输数据到主机cudaMemcpyAsync(h_z, d_z, M, cudaMemcpyDeviceToHost, cudaStreamDefault);// 同步等待设备所有任务完成cudaDeviceSynchronize();// 验证结果bool success = true;for (int i = 0; i < N; ++i) {if (h_z[i] != h_x[i] + h_y[i]) {success = false;break;}}if (success) {std::cout << "Array addition completed successfully!" << std::endl;}else {std::cout << "Error in array addition!" << std::endl;}// 释放内存cudaFree(d_x);cudaFree(d_y);cudaFree(d_z);cudaFreeHost(h_x);cudaFreeHost(h_y);cudaFreeHost(h_z);return 0;
}

运行结果:
在这里插入图片描述
通过这种方式,主机能够在 GPU 进行核函数计算时进行自身的运算,以实现主机和设备计算的重叠。

3 在非默认流中重叠多个核函数的执行

虽然在一个默认流中就可以实现主机计算和设备计算的并行,但是要实现多个核函数之间的并行必须使用多个 CUDA 流。

我们这里仅讨论使用多个非默认流的情况,使用非默认流时,核函数的执行配置中必须包含一个流对象。一个名为 my_kernel 的核函数可以用以下方法调用:

  • my_kernel<<<N_grid, N_block, N_shared, stream_id>>>(函数参数);

stream_id 是 CUDA 流的编号,说明核函数在编号为stream_id 的 CUDA 流中执行,而且使用了 N_shared 字节的动态共享内存,注意:如果使用非空流,但不想使用共享内存,则应将N_shared设置为0,不能忽略不写

  • my_kernel<<<N_grid, N_block, 0, stream_id>>>(函数参数);

3.1 重叠多个核函数的例子

#include <cuda_runtime.h>
#include <iostream>#define N 1024 * 1024
#define THREADS_PER_BLOCK 256// 核函数1:对数组每个元素加1
__global__ void kernelAddOne(float* data, int n) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < n) {data[idx] += 1.0f;}
}// 核函数2:对数组每个元素乘2
__global__ void kernelMultiplyTwo(float* data, int n) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < n) {data[idx] *= 2.0f;}
}int main() {// 定义主机和设备内存float* h_data, * d_data1, * d_data2;// 分配主机内存并初始化size_t size = N * sizeof(float);h_data = (float*)malloc(size);for (int i = 0; i < N; ++i) {h_data[i] = static_cast<float>(i);}// 分配设备内存cudaMalloc((void**)&d_data1, size);cudaMalloc((void**)&d_data2, size);// 将数据从主机传输到设备cudaMemcpy(d_data1, h_data, size, cudaMemcpyHostToDevice);cudaMemcpy(d_data2, h_data, size, cudaMemcpyHostToDevice);// 创建两个流cudaStream_t stream1, stream2;cudaStreamCreate(&stream1);cudaStreamCreate(&stream2);// 计算网格大小int gridSize = (N + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK;// 在不同流中启动核函数kernelAddOne << <gridSize, THREADS_PER_BLOCK, 0, stream1 >> > (d_data1, N);kernelMultiplyTwo << <gridSize, THREADS_PER_BLOCK, 0, stream2 >> > (d_data2, N);// 异步从设备传输数据到主机cudaMemcpyAsync(h_data, d_data1, size, cudaMemcpyDeviceToHost, stream1);cudaMemcpyAsync(h_data, d_data2, size, cudaMemcpyDeviceToHost, stream2);// 等待所有流完成cudaStreamSynchronize(stream1);cudaStreamSynchronize(stream2);// 清理cudaFree(d_data1);cudaFree(d_data2);cudaStreamDestroy(stream1);cudaStreamDestroy(stream2);free(h_data);std::cout << "Kernels executed in parallel streams!" << std::endl;return 0;
}

在 stream1 中调用 kernelAddOne,在 stream2 中调用 kernelMultiplyTwo。因为它们在不同流中,因此可以并行执行

利用 CUDA 流并发多个核函数可以提升 GPU 硬件的利用率,减少闲置的 SM,从而 从整体上获得性能提升。

4 在非默认流中重叠核函数的执行与数据传递

在上述代码中,我们发现“把数据从设备复制到主机”的代码使用的是cudaMemcpyAsync而不是以前使用的cudaMemcpy,前者便是后者的异步版本。

异步传输由 GPU 中的 DMA(direct memory access)直接实现,不需要主机参与。如果用同步的数据传输函数,主机在向一个流发出数据传输的命令后,将无法立刻获得控制权,必须等待数据传输完毕。

cudaMemcpyAsync 只比 cudaMemcpy 多一个参数。该函数的最后一个参数就是 所在流的变量,异步传输函数的原型为:

cudaError_t cudaMemcpyAsync ( 
void *dst, 
const void *src, 
size_t count, 
enum cudaMemcpyKind kind, 
cudaStream_t stream 
);

在使用异步的数据传输函数时,需要将主机内存定义为不可分页内存(non-pageable memory)或者固定内存(pinned memory)
如果将可分页内存传给 cudaMemcpyAsync 函数,则会导同步传输

4.1 不可分页内存

  • 可分页内存(Pageable Memory):默认情况下,主机(CPU)分配的内存都是“可分页内存“

  • 不可分页内存(Pinned Memory)或“分页锁定内存”:固定在物理内存中的,CUDA 使用 cudaMallocHost 函数和cudaHostAlloc来分配这种内存。

  • cudaError_t cudaMallocHost(void** ptr, size_t size); cudaError_t

  • cudaHostAlloc(void** ptr, size_t size, size_t flags);
    若函数 cudaHostAlloc 的第三个参数取默认值 cudaHostAllocDefault,则以上两个函数完全等价。

由以上函数分配的主机内存必须由如下函数释放:

  • cudaError_t cudaFreeHost(void* ptr);

如果不小心用了 free 函数释放不可分页主机内存,会出现运行错误。

4.2 示例分析

如果仅使用一个 CUDA 流(如默认流),那么以上 3 个操作在设备中一定是顺序的:
在这里插入图片描述
如果简单地将以上 3 个 CUDA 操作放入 3 个不同的流,相比仅使用一个 CUDA 流的情形依然不能得到加速,因为以上 3 个操作在逻辑上是有先后次序的。如果使用 3 个流,其执行流程可以理解如下:
在这里插入图片描述要利用多个流提升性能,就必须创造出在逻辑上可以并发执行的 CUDA 操作。一个方法是将以上 3 个 CUDA 操作都分成若干等份,然后在每个流中发布一个 CUDA 操作序列。 例如,使用两个流时,我们将以上 3 个 CUDA 操作都分成两等份。在理想情况下,它们的执行流程可以如下:
在这里插入图片描述
注意,这里的每个 CUDA 操作所处理的数据量只有使用一个 CUDA 流时的一半

如果 H2D、KER、和 D2H 这 3 个 CUDA 操作的执行时间都相同,那么就能有效地隐藏一个 CUDA 流中两个 CUDA 操作的执行时间,使得总的执行效率相比使用单个 CUDA 流的情形提升到 6/4 = 1:5 倍。

我们可以类似地分析使用更多流的情形。例如,当使用 4 个流并将每个流中的 CUDA 操 作所处理的数据量变为最初的 1/4 时,在理想的情况下可以得到如下执行流程:
在这里插入图片描述
此时,总的执行效率相比使用单个 CUDA 流的情形提升到 12/6 = 2 倍。不难理解,随着流的数目的增加,在理想情况下能得到的加速比将趋近于 3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/883671.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

操作系统期末|考研复习知识点汇总 - 持续更新

本文将根据个人学习进度对b站王道408课程以及题目考察的知识点进行整合&#xff0c;视频中详细的导图将会直接复用&#xff0c;并且将会对一些重点知识进行扩展以及一些思维导图的补充&#xff0c;(目前第三章内容正在整理中……由于第三章内容繁多且都是重点&#xff0c;习题量…

.NET Core WebApi第3讲:第一个WebApi项目、WebApi开发三种模型

一、.NEt Core 1、运行模板项目 1&#xff09;仍然有controllers&#xff0c;说明WebApi是基于MVC模式的&#xff0c;只是对比之下这里没有MVC中的views。 因为WebApi只会向前台发送数据&#xff0c;不会向前台发送HTML页面。 2、验证模板项目的api 1&#xff09;法1&#xf…

微服务之间调用,OpenFeign传递用户(RequestInterceptor接口)

场景&#xff1a;微服务之黑马商城项目-登录拦截器在网关完成用户的校验&#xff0c;并将用户信息&#xff08;用户id&#xff09;存入请求头&#xff0c;假设将购物车里面的商品进行结算就会生成订单并清空购物车&#xff0c;这里涉及到了交易服务模块远程调用购物车模块&…

单细胞数据分析(一):10X数据生成seurat数据对象

文章目录 介绍加载R包数据链接导入数据过滤细胞:移除双重细胞合并所有seurat数据对象输出结果系统信息介绍 在单细胞基因组学研究中,Seurat是一个流行的R包,用于单细胞基因表达数据的分析和探索。以下是如何从10X基因注释数据生成Seurat数据对象,并对该数据进行过滤的步骤…

RHCE的学习(8)

动态网站 lnmp&#xff08;LAMP&#xff09; 解析index.php界面 &#xff08;1&#xff09;预配&#xff0c;确保服务能够被访问 systemctl stop firewalld setenforce 0 &#xff08;2&#xff09;安装nginx服务 mount /dev/sr0 /mnt cat /etc/yum.repos.d/base.repo dnf …

NVR设备ONVIF接入平台EasyCVR视频融合平台智慧小区视频监控系统建设方案

一、方案背景 智慧小区构成了“平安城市”建设的基石。随着社会的进步&#xff0c;社区安全问题逐渐成为公众关注的热点。诸如高空抛物、乱丢垃圾、破坏车辆、入室盗窃等不文明行为和违法行为频繁出现。目前&#xff0c;许多小区的物业管理和安全防护系统仍然较为简单和陈旧&a…

UML总结

零&#xff1a;学习链接 UML_哔哩哔哩_bilibili 一&#xff1a;UML概述 二&#xff1a;类图 类图&#xff08;Class Diagram&#xff09;是统一建模语言&#xff08;UML&#xff09;中一种重要的图形表示&#xff0c;用于描述系统中的类及其之间的关系。它是面向对象设计中常…

软件已死,数据永生?

大数据产业创新服务媒体 ——聚焦数据 改变商业 你有没有注意到&#xff0c;你的生活正在被数据所支配&#xff1f; 我们看似在掌控自己的每一次点击、每一次搜索、每一个消费选择&#xff0c;但实际上&#xff0c;背后隐藏着庞大的数据网络。每一个点赞、每一次搜索&#xff…

Java非对称加密:RSA 数据加密与解密、数字签名与验签

Java常用的加密与解密系列文章: 《Java编码方式:Base64 编码与解码》 《Java消息摘要:MD5 验证数据完整性、密码的加密》 《Java消息摘要:SHA 验证数据完整性、密码的加密》 《Java对称加密:DES、3DES 数据加密标准》 《Java对称加密:AES 高级加密标准》 《Java非对称加密…

java质数的判断 C语言指针变量的使用

1. public static void main(String[] args) {Scanner scnew Scanner(System.in);System.out.println("请输入一个值");int num sc.nextInt();boolean flagtrue;for (int i2;i<num;i){if (num%i0){flagfalse;break;}}if (flag){System.out.println(num"是一…

深度学习:权重参数相关知识(深度学习入门:基于Python的理论与实现 (斋藤康毅)))

在神经网络的学习中&#xff0c;权重的初始值特别重要&#xff0c;经常关系到神经网络的学习能否成功。 第一点&#xff0c;权重初始值不能设置为0&#xff0c;严格说权重初始值不能设为同样的值&#xff0c;是因为在误差反向传播法中&#xff0c;所有权重值都会进行相同的更新…

云联网对等连接--实现内网互通

云联网 今天给大家介绍一款产品&#xff0c;腾讯云的云联网。 云联网&#xff1a;为您提供云上私有网络间&#xff08;VPC&#xff09;、VPC 与本地数据中心间&#xff08;IDC&#xff09;内网互联的服务&#xff0c;具备全网多点互联、路由自学习、链路选优及故障快速收敛等…

采用指针作为函数参数

在main.cpp里输入程序如下&#xff1a; #include <iostream> //使能cin(),cout(); #include <iomanip> //使能setbase(),setfill(),setw(), //setprecision(),setiosflags()和resetiosflags(); using namespace std; //告诉编译器使用std标准程序库; void…

kali——tcpdump的使用

目录 前言 使用方法 监听指定网卡 将抓取的数据包保存到指定文件 读取数据包 前言 定义&#xff1a;tcpdump 是 Linux 系统下的一个强大的命令行式数据包嗅探工具&#xff0c;它能够实时捕获网络接口上的数据包&#xff0c;并将这些数据包的头部信息或完整内容显示出来或保…

Java面向对象编程进阶(四)

Java面向对象编程进阶&#xff08;四&#xff09; 一、equals()方法的使用二、toString()方法的使用三、复习 一、equals()方法的使用 适用性&#xff1a;任何引用数据都可以使用。 自定义的类在没有重写Object中equals()方法的情况下&#xff0c;调用的就是Object类中声明的…

011:软件卸载工具TotalUninstall安装教程

摘要&#xff1a;本文详细介绍软件卸载工具TotalUninstall安装流程。 一、软件介绍 TotalUninstall是一款功能强大的卸载与清理工具&#xff0c;它能够彻底卸载不需要的应用程序&#xff0c;并清除相关的注册表项、文件残留和临时文件&#xff0c;确保系统干净无残留&#xff…

每日一题之电话号码的字母组合

给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits "23" 输出&#…

2025选题|基于Hadoop的物品租赁系统的设计与实现

作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参与学生毕业答辩指导&#xff0c;…

星海智算:用户如何使用公共存储

公共存储是平台下载好的一些模型、数据集&#xff0c;只读方便拷贝或者直接引入。 公共存储会由官方定时更新,更新内容来源于用户反馈,会将反馈的模型下载到公共存储,供用户拷贝到实例本地使用。 01 下载模型 直接从公共存储路径 /mnt/pub_data/ 下载所需模型。 02 复制…

五周年,继续破浪前行

五周年&#xff0c;TapData 再一次带着自己的“乘风破浪”大队&#xff0c;在一个阳光明媚的日子里&#xff0c;把生日过在了海上。 头顶日升日落&#xff0c;这条属于全体 Tap-pers 的航船&#xff0c;再次校准航向&#xff0c;在船长的带领下&#xff0c;驶向下一个晴好的明…