《CUDA编程:基础与实践》读书笔记(4):CUDA流
1. CUDA流
一个CUDA流指的是由主机发出的在一个设备中执行的CUDA操作序列。除主机端发出的流之外,还有设备端发出的流,但本文不考虑后者。一个CUDA流中的各个操作按照主机发布的次序执行;但来自两个不同CUDA流的操作不一定按照某个次序执行,有可能是并发或者交错地执行。
任何CUDA操作都存在于某个CUDA流中,如果没有明确指定CUDA流,那么所有CUDA操作都是在默认流中执行的。非默认CUDA流由cudaStream_t类型的变量表示,它由如下CUDA运行时API产生与销毁:
cudaError_t cudaStreamCreate(cudaStream_t* pStream);
cudaError_t cudaStreamDestroy(cudaStream_t stream);
为了检查CUDA流中的所有操作是否都在设备中执行完毕,可以使用如下函数:
//阻塞主机直到stream中的所有操作都执行完毕
cudaError_t cudaStreamSynchronize(cudaStream_t stream);
//不阻塞主机,只检查stream中的所有操作是否都执行完毕,若是则返回cudaSuccess,否则返回cudaErrorNotReady
cudaError_t cudaStreamQuery(cudaStream_t stream);
为了产生多个相互独立的CUDA流、实现不同CUDA流之间的并发,主机在向某个CUDA流中发布命令后必须马上获取程序控制权,不等待该CUDA流中的命令在设备中执行完毕。下文将介绍主机如何在向某个CUDA流发布命令后马上取得控制权。此外,也可以在主机端使用多个线程控制多个CUDA流。
2. 核函数与主机的重叠执行
下面是默认CUDA流中数组相加的例子:
cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice);
cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice);
add<<<grid_size, block_size>>>(d_x, d_y, d_z);
cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost);
从设备的角度看,以上4个CUDA操作将在默认CUDA流中按顺序依次执行。从主机的角度看,数据传输是同步的(或者说是阻塞的),比如说主机在执行前两个cudaMemcpy语句时,会等待该命令执行完毕再继续往下走,所以在进行数据传输时,主机是闲置的,不能进行其它操作。不同的是,核函数的启动是异步的(或者说是非阻塞的),意思是主机发出调用核函数的命令后,不会等待命令执行完毕,而会立刻取得程序控制权,然后紧接着发出最后一个cudaMemcpy命令,但是该命令不会立即被执行,因为这是默认流中的CUDA操作,必须等待前一个CUDA操作(即核函数的调用)执行完毕才会开始执行。
根据上述分析可知,主机在发出核函数调用命令后会立刻继续执行接下来的命令。如果下一条命令是主机的某个计算任务,那么就可以实现核函数与主机计算任务的并行计算。
3. 核函数与核函数的重叠执行
因为同一个CUDA流中的CUDA操作在设备中是顺序执行的,所以要实现多个核函数之间的并行就必须使用多个CUDA流。在使用的多个CUDA流中可以有一个默认流,但此时各个流之间并不完全独立,本文不讨论这种情况,只讨论使用多个非默认流的情况。在非默认流中调用核函数时,执行配置必须包含一个流对象,一个名为my_kernel(...)的核函数只能用如下三种调用方式之一:
//N_grid是网格大小,最一般的情形是一个dim3类型的结构体,简单情况下可以是一个整数
//N_block是线程块大小,最一般的情形是一个dim3类型的结构体,简单情况下可以是一个整数
//N_shared是核函数中使用的动态共享内存的字节数,如果没有则设为0
//stream是cudaStream_t类型的CUDA流对象
my_kernel<<<N_grid, N_block>>>(...);
my_kernel<<<N_grid, N_block, N_shared>>>(...);
my_kernel<<<N_grid, N_block, N_shared, stream>>>(...);
下面的例子简单展示了如何使用非默认CUDA流重叠执行多个核函数:
#include "cuda_runtime.h"
void __global__ my_kernel()
{
// do some calculations
}
int main(void)
{
const int NUM_STREAMS = 16;
const int block_size = 128;
const int grid_size = 8;
cudaStream_t streams[NUM_STREAMS];
for (int n = 0; n < NUM_STREAMS; ++n)
{
cudaStreamCreate(&(streams[n]));
}
for (int n = 0; n < NUM_STREAMS; ++n)
{
my_kernel<<<grid_size, block_size, 0, streams[n]>>>();
}
for (int n = 0; n < NUM_STREAMS; ++n)
{
cudaStreamDestroy(streams[n]);
}
return 0;
}
利用CUDA流并发执行多个核函数可以提升GPU硬件的利用率,减少闲置的SM,从而整体上获得性能提升。但当所有CUDA流中对应核函数的线程数总和超过一定阈值后,再增加CUDA流的数量就不会带来更高的加速比了,反而可能使程序的性能下降。制约加速比的因素是GPU的计算资源。
4. 核函数与数据传输的重叠执行
要实现核函数与数据传输的并发,必须让这两个操作处于不同的非默认流,而且数据传输必须使用cudaMemcpy的异步版本,即cudaMemcpyAsync函数。如果使用同步的数据传输函数,主机向一个流发出输出传输命令后就必须等待数据传输完毕,这样核函数与数据传输的重叠也就无法实现。异步传输函数的原型是:
cudaError_t cudaMemcpyAsync(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind, cudaStream_t stream);
在使用异步数据传输函数时,需要将主机内存定义为不可分页内存,这样在程序运行期间操作系统就不会改变主机内存的物理地址。如果给cudaMemcpyAsync函数传入的主机内存是可分页内存,那么函数就会退化到cudaMemcpy,从而导致同步传输,无法达到核函数与数据传输重叠执行的效果。不可分页主机内存的分配与释放可以用如下函数:
cudaError_t cudaMallocHost(void **ptr, size_t size);
cudaError_t cudaFreeHost(void *ptr);
下面给出一个使用CUDA流重叠执行核函数和数据传输的例子:
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
const int N = 1 << 22;
const int M = sizeof(float) * N;
const int NUM_STREAMS = 64;
cudaStream_t streams[NUM_STREAMS];
void __global__ add(const float* x, const float* y, float* z, int N)
{
const int n = blockDim.x * blockIdx.x + threadIdx.x;
if (n < N)
{
z[n] = x[n] + y[n];
}
}
int main(void)
{
float *h_x, *h_y, *h_z;
cudaMallocHost(&h_x, M);
cudaMallocHost(&h_y, M);
cudaMallocHost(&h_z, M);
for (int n = 0; n < N; ++n)
{
h_x[n] = 1.23f;
h_y[n] = 2.34f;
}
float *d_x, *d_y, *d_z;
cudaMalloc(&d_x, M);
cudaMalloc(&d_y, M);
cudaMalloc(&d_z, M);
for (int i = 0; i < NUM_STREAMS; i++)
{
cudaStreamCreate(&(streams[i]));
}
int N1 = N / NUM_STREAMS;
int M1 = M / NUM_STREAMS;
for (int i = 0; i < NUM_STREAMS; i++)
{
int off = i * N1;
cudaMemcpyAsync(d_x + off, h_x + off, M1, cudaMemcpyHostToDevice, streams[i]);
cudaMemcpyAsync(d_y + off, h_y + off, M1, cudaMemcpyHostToDevice, streams[i]);
int block_size = 128;
int grid_size = (N1 - 1) / block_size + 1;
add<<<grid_size, block_size, 0, streams[i]>>>(d_x + off, d_y + off, d_z + off, N1);
cudaMemcpyAsync(h_z + off, d_z + off, M1, cudaMemcpyDeviceToHost, streams[i]);
}
for (int i = 0; i < NUM_STREAMS; i++)
{
cudaStreamDestroy(streams[i]);
}
cudaFreeHost(h_x);
cudaFreeHost(h_y);
cudaFreeHost(h_z);
cudaFree(d_x);
cudaFree(d_y);
cudaFree(d_z);
return 0;
}
《CUDA编程:基础与实践》读书笔记(4):CUDA流的更多相关文章
- 《Java并发编程的艺术》读书笔记:二、Java并发机制的底层实现原理
二.Java并发机制底层实现原理 这里是我的<Java并发编程的艺术>读书笔记的第二篇,对前文有兴趣的朋友可以去这里看第一篇:一.并发编程的目的与挑战 有兴趣讨论的朋友可以给我留言! 1. ...
- 《jQuery基础教程》读书笔记
最近在看<jQuery基础教程>这本书,做了点读书笔记以备回顾,不定期更新. 第一章第二章比较基础,就此略过了... 第三章 事件 jQuery中$(document).ready()与j ...
- 《Java并发编程的艺术》读书笔记:一、并发编程的目的与挑战
发现自己有很多读书笔记了,但是一直都是自己闷头背,没有输出,突然想起还有博客圆这么个好平台给我留着位置,可不能荒废了. 此文读的书是<Jvava并发编程的艺术>,方腾飞等著,非常经典的一本 ...
- cuda编程基础
转自: http://blog.csdn.net/augusdi/article/details/12529247 CUDA编程模型 CUDA编程模型将CPU作为主机,GPU作为协处理器(co-pro ...
- Java并发编程实践读书笔记(2)多线程基础组件
同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...
- Java并发编程实践读书笔记(5) 线程池的使用
Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...
- Java并发编程实践(读书笔记) 任务执行(未完)
任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元. 任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任 ...
- Java并发编程实践读书笔记(1)线程安全性和对象的共享
2.线程的安全性 2.1什么是线程安全 在多个线程访问的时候,程序还能"正确",那就是线程安全的. 无状态(可以理解为没有字段的类)的对象一定是线程安全的. 2.2 原子性 典型的 ...
- Java并发编程实践读书笔记(4)任务取消和关闭
任务的取消 中断传递原理 Java中没有抢占式中断,就是武力让线程直接中断. Java中的中断可以理解为就是一种简单的消息机制.某个线程可以向其他线程发送消息,告诉你“你应该中断了”.收到这条消息的线 ...
- Java并发编程实践读书笔记(3)任务执行
类似于Web服务器这种多任务情况时,不可能只用一个线程来对外提供服务.这样效率和吞吐量都太低. 但是也不能来一个请求就创建一个线程,因为创建线程的成本很高,系统能创建的线程数量是有限的. 于是Exec ...
随机推荐
- MongoDB聚合类操作
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果.有点类似sql语句中的 count(*) 语法:db.tablename.aggregat ...
- Go语言单元测试的执行
Go 语言推荐测试文件和源代码文件放在同一目录下,测试文件以 _test.go 结尾.比如,当前 package 有 calc.go 一个文件,我们想测试 calc.go 中的 Add 和 Mul 函 ...
- 初识GO语言--并发
- 【2024.08.15】NOIP2024暑假集训模拟赛(13)
[2024.08.15]NOIP2024暑假集训模拟赛(13) T1 先找能构成回文的最长前缀和后缀(长度相同的),然后在任意一边的基础上扩展,看能否接一个回文串. #include<bits/ ...
- js实现浏览器后退页面刷新
最近在开发中遇到一个问题: 在一个列表页面,点击进入详情,详情页面对其状态操作,其详情页面有做修改,然后点击浏览器后退,返回到列表页,在列表页面状态还是操作之前的,为解决状态统一需要手动刷新改列表页. ...
- 如何使用Flask编写一个网站
使用Flask编写一个网站是一个相对简单且有趣的过程.Flask是一个用Python编写的轻量级Web应用框架.它易于上手,同时也非常强大,适合构建从简单的博客到复杂的Web应用的各种项目.以下是一个 ...
- C# 请求 form-data格式的 接口 POSTMAN form-data
HttpClient _httpClient = new HttpClient(); var postContent = new MultipartFormDataContent(); string ...
- 3张大图剖析HttpClient和IHttpClientFactory在解决DNS解析问题上的殊途同归
在开发者便利度角度,我们很轻松地使用HttpClient对象发出HTTP请求,只需要关注应用层协议的BaseAddr.Url.ReqHeader.timeout. 实际在HttpClient在源码级别 ...
- NZOJ 模拟赛4
T1 数字游戏 大家列队后,都觉得累了,于是一起坐到院子中的草地上休息.这时Anna突然想跟她的最大竞争对手Cici玩一个数字游戏,她要你编写程序帮助她取得胜利. 第i次游戏初始时有一个整数N_i(1 ...
- kafka之介绍
Kafka 由于高吞吐量.可持久化.分布式.支持流数据处理等特性而被广泛应用.但当前关于Kafka原理及应用的相关资料较少,在我打算编写本文时,还没有见到中文版本的Kafka相关书籍,对于初学者甚至是 ...