CUDA编程:stream和Concurrency初探

总是在讲概念和写一些简单的hello world似乎有些无聊,为了更好的去理解,咱们下一篇将以实战为主,使用tensorrt编写自己的plugin(其实是一直在介绍概念,我都有些困了,咱们一起写个项目玩玩),但是在实操之前我们还需要介绍几个重要的概念,在写项目时我们会用到。

CUDA程序并发性可分为两种:

1、kernel level concurrency:一个task由GPU上多个thread并行执行的。(我们前几张介绍的都是kernel level的开发)

2、grid level level concurrency:多个task在GPU上同时执行。

(一切为了速度,冲冲冲!)

多个task并行执行会遇到一个问题,假设后续某个任务需要前面某个任务的结果,如果它们都同一时间执行,程序会崩溃的。那么,自然而然我们引出一个概念:stream。

CUDA stream是将主机上的多个kernel进行某种顺序的排序。通过stream可以控制多个kernel在设备上的顺序。不同的stream互不影响。通常情况下,执行kernel比GPU-CPU传输数据的时间要长。因此,某些情况下可以将kernel的执行操作和数据传输放到不同的stream中,用kernel的时间掩盖传输时间,缩短程序运行时间。

CUDA stream可分为两种操作:

1、同步:此状态会阻塞CPU进程,直到kernel操作完成。

2、异步:此状态在唤醒kernel函数后立刻将控制权交给CPU。

还记得我们上一节介绍的SM等硬件的知识吗?这里也一样,从软件层面上看,不同的stream是同时进行的,但在硬件层面上还需要去争夺SM资源,可能需要相互等待。

CUDA stream可以显式或隐式调用。在前几张实例中,虽然我们在代码中没有任何stream的操作,但实际上系统会自动分配一个隐式stream,所有的kernel都在这一个stream上,如以下操作,cudaMemcpy会阻塞cpu进程,直到数据传输操作完成。

cudaMemcpy(…, cudaMemcpyHostToDevice);
kernel<<>>(…);
cudaMemcpy(…, cudaMemcpyDeviceToHost);
但假如我们相对多个kernel的执行顺序进行操作,则必须申请一个或多个显式stream进行管理。例如在以下情形中:

1、重叠host和device的计算。

2、重叠host计算与cpu-gpu的数据传输。

3、重叠device计算与cpu-gpu的数据传输。

4、gpu的并发计算

cudaMemcpyAsync可以执行异步操作,在数据传输过程中,将操作权交给cpu进行控制。

cudaError_t cudaMemcpyAsync(void* dst, const void* src, size_t count,
cudaMemcpyKind kind, cudaStream_t stream = 0);
申请cuda stream:

cudaError_t cudaStreamCreate(cudaStream_t* pStream);
在异步传输数据时必须使用固定/不可分页的主机内存,可使用以下APT申请:

cudaError_t cudaMallocHost(void **ptr, size_t size);
cudaError_t cudaHostAlloc(void **pHost, size_t size, unsigned int flags);
ok,插播一下,什么是不可分页的内存?为啥要用它?

系统为了扩大内存空间会在硬盘上开辟一块虚拟内存。虚拟地址不能访问物理意义上的内存,需要内存管理单元将其映射到某个物理页面,然后转换为物理地址,CPU才能进行访问。例如我们平时写程序打印数据地址,其实就是一个虚拟地址,该地址通过操作系统底层的转换去找到真实的数据位置(在硬盘上的位置)。

分页内存:将暂时不会被用到的虚拟内存页面的内容,存储到磁盘上。可以理解为程序读取数据,先指向分页内存表,然后查找对应数据位置。(访问—>虚拟页面—>真实物理地址,可能在内存上也可能在硬盘上)

非分页内存:虚拟内存对应映射到的物理内存是常驻在物理内存中。(内存上的这块空间是我的了)(访问—>内存)

速度上,非分页内存比分页内存快的得多,所有开辟空间需要在非分页内存上进行。(详细的解释,可以去参考操作系统,如果我的理解有啥不对的地方,欢迎大佬们指正)

还有一个原因:上述申请的空间在整个程序中是一直存在的(即这块地盘在程序结束之前一直是我的),如果在分页内存中,则操作系统可以随时自由的更改主机虚拟内存的物理地址。如果一段时间不用,该数据可能被操作系统移到另一个位置,即当前的内存空间储存其他数据,那么当CUDA运行时调用该块内存,会造成程序崩溃。

stream的声明,创建,销毁以及在kernel中的调用:

cudaStream_t stream;

cudaStreamCreate(&stream);

cudaError_t cudaStreamDestroy(cudaStream_t stream);

kernel_name<<>>(argument list);
调用cudaStreamDestroy时,假如stream中仍有未完成的工作,cudaStreamDestroy会立即返回(将控制权交给CPU),并在所有stream中的操作结束后,释放相关资源。

cudaStreamSynchronize:强制主机等待所有stream中的操作结束。

cudaError_t cudaStreamSynchronize(cudaStream_t stream);
cudaStreamQuery:查询stream中操作是否完成,不会阻塞主机进程。

cudaError_t cudaStreamQuery(cudaStream_t stream);
ok,来一段代码试试火力。

for (int i = 0; i < nStreams; i++) {
int offset = i * bytesPerStream;
cudaMemcpyAsync(&d_a[offset], &a[offset], bytePerStream, streams[i]);
kernel<>(&d_a[offset]);
cudaMemcpyAsync(&a[offset], &d_a[offset], bytesPerStream, streams[i]);
}
for (int i = 0; i < nStreams; i++) {
cudaStreamSynchronize(streams[i]);
}
共有nStream,第一个for循环中每次循环只花费了唤醒kernel的时间,然后立刻将控制权交给CPU,第二个for循环阻塞cpu进程,等待操作完成,整体时间消耗如下图所示:
CUDA编程:stream和Concurrency初探_第1张图片

图中,虽然不同操作隶属不同的stream,但数据传输不是同时进行,甚至没有一点儿重叠,这是为嘛?因为传输过程需要通过PCIe总线进行传输,同一时间段只能传输一种方向的stream。如果是双工PCIe,则可同时传递两者不同方向的stream。

注:同一时刻,并发的kernel的最大数量受硬件控制。不同设备可能支持16路或32路等并发模式。实际并发数量还受到shared memory和寄存器等影响。

上面我们提到多个stream在硬件层面实际是互相等待的,CUDA给我们提供一个APT,可以定义stream的优先级:

cudaError_t cudaStreamCreateWithPriority(cudaStream_t* pStream, unsigned int flags,
int priority);
优先级更高的stream可能会抢占等级低的stream的资源,假如一个低等级的stream在处理操作时,可能某个warp已经处理完毕正在等待下一波thread,此时优先级更高的stream可能会先抢占这个warp进行操作。优先等级仅对计算有关,与数据传输没有任何影响。

查询当前设备中的stream优先级范围:

cudaError_t cudaDeviceGetStreamPriorityRange(int *leastPriority,
int *greatestPriority);
leastPriority:最小的优先级

greatestPriority:最高的优先级

注:较老的的显卡可能不支持优先级属性,则least=great=0。

CUDA Events

CUDA Events是cuda stream中的标记。(类似跑马拉松,把整个路程分为一段段的,每一段的路标就是一个events)Event只是一个标志,你可以用来查询stream的状态:

1、同步stream的执行

2、监控device进度

CUDA允许在任何一个点插入event,并查询该event是否完成。只有当该事件段中所有操作都被满足,event才会标志为完成。

events的声明,创建,销毁:

cudaEvent_t event;

cudaError_t cudaEventCreate(cudaEvent_t* event);

cudaError_t cudaEventDestroy(cudaEvent_t event);
cudaEventDestroy与cudaStreamDestroy一样,事件未完成立刻返回,并在该事件结束后销毁相关资源。

events记录stream的操作的某一点是否完成,将event插入stream中:

cudaError_t cudaEventRecord(cudaEvent_t event, cudaStream_t stream = 0);
传递的事件可用于等待或测试指定的stream中先前所有操作是否完成。

等待操作,阻塞主机:

cudaError_t cudaEventSynchronize(cudaEvent_t event);
cudaEventSynchronize与cudaStreamSynchronize不同之处在于,cudaEventSynchronize可以使主机等待stream中某一点的操作完成。

测试操作,不阻塞主机:

cudaError_t cudaEventQuery(cudaEvent_t event);
测量两个event之间的时间,单位ms:

cudaError_t cudaEventElapsedTime(float* ms, cudaEvent_t start, cudaEvent_t stop);
event开始与停止不需要与某个特定的stream绑定,(俺只是用来查询的,不管你的操作资源是否被强占等等,俺只是一个标志)。

你可能感兴趣的:(服务器,java,网络)