CUDA 7流简化并发

异构计算是指有效使用系统中的所有处理器,包括CPU和GPU。为此,应用程序必须在多个处理器上同时执行功能。CUDA应用程序通过在(按顺序执行的命令序列)中,执行异步命令来管理并发性。不同的流可能同时执行,或彼此相对执行命令。

在不指定流的情况下执行异步CUDA命令时,运行时runtime将使用默认流。在CUDA 7之前,默认流是特殊流,它与设备上的所有其他流隐式同步。

CUDA 7引入了大量强大的新功能,其中包括为每个主机线程使用独立默认流的新选项,从而避免了对传统默认流的序列化。本文将展示这如何简化CUDA程序中内核与数据副本之间的并发。

Asynchronous Commands in CUDA

As described by the CUDA C Programming Guide, asynchronous commands return control to the calling host thread before the device has finished the requested task (they are non-blocking). These commands are:

  • Kernel launches;
  • Memory copies between two addresses to the same device memory;
  • Memory copies from host to device of a memory block of 64 KB or less;
  • Memory copies performed by functions with the Async suffix;
  • Memory set function calls.

Specifying a stream for a kernel launch or host-device memory copy is optional; you can invoke CUDA commands without specifying a stream (or by setting the stream parameter to zero). The following two lines of code both launch a kernel on the default stream.

如CUDA C编程指南所述,异步命令在设备完成请求的任务之前,将控制权返回给调用主线程(它们是非阻塞的)。这些命令是:

  • 内核启动;
  • 在两个地址之间复制内存到相同的设备内存;
  • 从主机到设备的内存副本,大小为64 KB或更少;
  • 带Async后缀的功能执行的内存副本;
  • 内存设置函数调用。

为内核启动或主机设备内存副本指定流是可选的;可以在不指定流的情况下(或通过将stream参数设置为零)调用CUDA命令。以下两行代码都在默认流上启动内核。

kernel<<< blocks, threads, bytes >>>();    // default stream

kernel<<< blocks, threads, bytes, 0 >>>(); // stream 0

The Default Stream

如果并发性对性能不重要,则默认流很有用。在CUDA 7之前,每个设备都有一个用于所有主机线程的默认流,这会导致隐式同步。如《 CUDA C编程指南》中的“隐式同步”部分所述,如果主机线程向它们之间的默认流发出任何CUDA命令,则来自不同流的两个命令不能同时运行。

CUDA 7引入了一个新选项,即每线程默认流,它具有两个作用。首先,它为每个主机线程提供自己的默认流。这意味着由不同的主机线程发布到默认流的命令可以同时运行。其次,这些默认流是常规流。这意味着默认流中的命令可以与非默认流中的命令同时运行。

为了使每个线程默认CUDA流7或更高版本,可以用编译nvcc命令行选项--default-stream per-thread,或#defineCUDA_API_PER_THREAD_DEFAULT_STREAM预处理宏包括CUDA头(前cuda.hcuda_runtime.h)。重要的是要注意:#define CUDA_API_PER_THREAD_DEFAULT_STREAM编译代码时,不能使用.cu文件中的此功能,nvcc因为nvcc隐式包括cuda_runtime.h在翻译单元的顶部。

To enable per-thread default streams in CUDA 7 and later, you can either compile with the nvcc command-line option --default-stream per-thread, or #define the CUDA_API_PER_THREAD_DEFAULT_STREAM preprocessor macro before including CUDA headers (cuda.h or cuda_runtime.h). It is important to note: you cannot use #define CUDA_API_PER_THREAD_DEFAULT_STREAM to enable this behavior in a .cu file when the code is compiled by nvcc because nvcc implicitly includes cuda_runtime.h at the top of the translation unit.

A Multi-Stream Example

Let’s look at a trivial example. The following code simply launches eight copies of a simple kernel on eight streams. We launch only a single thread block for each grid so there are plenty of resources to run multiple of them concurrently. As an example of how the legacy default stream causes serialization, we add dummy kernel launches on the default stream that do no work. Here’s the code.

看一个简单的例子。以下代码仅在八个流上启动一个简单内核的八个副本。为每个网格仅启动一个线程块,有大量资源可以同时运行多个线程。作为传统默认流如何导致序列化的一个示例,在默认流上添加了无效的虚拟内核启动。这是代码。

const int N = 1 << 20;

__global__ void kernel(float *x, int n)

{

int tid = threadIdx.x + blockIdx.x * blockDim.x;

for (int i = tid; i < n; i += blockDim.x * gridDim.x) {

x[i] = sqrt(pow(3.14159,i));

}

}

int main()

{

const int num_streams = 8;

cudaStream_t streams[num_streams];

float *data[num_streams];

for (int i = 0; i < num_streams; i++) {

cudaStreamCreate(&streams[i]);

cudaMalloc(&data[i], N * sizeof(float));

// launch one worker kernel per stream

kernel<<<1, 64, 0, streams[i]>>>(data[i], N);

// launch a dummy kernel on the default stream

kernel<<<1, 1>>>(0, 0);

}

cudaDeviceReset();

return 0;

}

首先,让我们通过不带任何选项的编译来检查传统行为。

nvcc ./stream_test.cu -o stream_legacy

可以在NVIDIA Visual Profiler(nvvp)中运行该程序,以获得显示所有流和内核启动的时间线。图1显示了在配备NVIDIA GeForce GT 750M(开普勒GPU)的Macbook Pro上生成的内核时间轴。可以在默认流上看到虚拟内核的极小条,以及如何导致所有其它流序列化。

当任何交错内核发送到默认流时,一个简单的多流示例不会实现并发

尝试新的每线程默认流。

nvcc --default-stream per-thread ./stream_test.cu -o stream_per-thread

图2显示了nvvp的结果。可以看到九个流之间的完全并发性:默认流(在本例中映射到流14)和创建的其它八个流。注意,虚拟内核运行得如此之快,以至于很难在该映像中看到默认流上有八个调用。

图2:使用新的每线程默认流选项的多流示例,该选项支持完全并发执行。

A Multi-threading Example

Let’s look at another example, designed to demonstrate how the new default stream behavior makes it easier to achieve execution concurrency in multi-threaded applications. The following example creates eight POSIX threads, and each thread calls our kernel on the default stream and then synchronizes the default stream. (We need the synchronization in this example to make sure the profiler gets the kernel start and end timestamps before the program exits.)

另一个示例旨在演示新的默认流,如何使在多线程应用程序中更容易实现执行并发。下面的示例创建八个POSIX线程,每个线程在默认流上调用内核,然后同步默认流。(需要进行同步,确保事件控制器在程序退出之前,获得内核的开始和结束时间戳记。)

#include <pthread.h>

#include <stdio.h>

const int N = 1 << 20;

__global__ void kernel(float *x, int n)

{

int tid = threadIdx.x + blockIdx.x * blockDim.x;

for (int i = tid; i < n; i += blockDim.x * gridDim.x) {

x[i] = sqrt(pow(3.14159,i));

}

}

void *launch_kernel(void *dummy)

{

float *data;

cudaMalloc(&data, N * sizeof(float));

kernel<<<1, 64>>>(data, N);

cudaStreamSynchronize(0);

return NULL;

}

int main()

{

const int num_threads = 8;

pthread_t threads[num_threads];

for (int i = 0; i < num_threads; i++) {

if (pthread_create(&threads[i], NULL, launch_kernel, 0)) {

fprintf(stderr, "Error creating threadn");

return 1;

}

}

for (int i = 0; i < num_threads; i++) {

if(pthread_join(threads[i], NULL)) {

fprintf(stderr, "Error joining threadn");

return 2;

}

}

cudaDeviceReset();

return 0;

}

不进行任何编译来测试旧版默认流的行为。

nvcc ./pthread_test.cu -o pthreads_legacy

当在nvvp中运行此命令时,看到单个流,即默认流,其中所有内核启动均已序列化,如图3所示。

图3:具有旧式默认流行为的多线程示例:所有八个线程都已序列化。

使用新的每线程默认流选项进行编译。

nvcc --default-stream per-thread ./pthread_test.cu -o pthreads_per_thread

图4显示了使用每个线程的默认流时,每个线程会自动创建一个新流,并且它们不会同步,因此所有八个线程的内核同时运行。

图4:具有每个线程默认流的多线程示例:来自所有八个线程的内核同时运行。

Tips提示

进行并发编程时,还需要记住一些其它事项。

  • 切记:对于每个线程的默认流,就同步和并发而言,每个线程中的默认流的行为与常规流相同。对于旧式默认流,情况并非如此。
  • --default-stream选项适用于每个编译单元,确保将其应用于nvcc需要它的所有命令行。
  • cudaDeviceSynchronize()继续使用新的每线程默认流选项同步设备上的所有内容。如果只想同步单个流,使用cudaStreamSynchronize(cudaStream_t stream),如第二个示例中所示。
  • 从CUDA 7开始,还可以使用句柄显式访问每个线程的默认流cudaStreamPerThread,并且可以使用句柄访问旧式默认流cudaStreamLegacy。注意,cudaStreamLegacy碰巧将它们混合在程序中,则仍与每个线程默认流进行隐式同步。
  • 可以通过将cudaStreamNonBlocking标志传递到cudaStreamCreate()来创建与旧式默认流同步的非阻塞流

CUDA 7流简化并发的更多相关文章

  1. CUDA 7 Stream流简化并发性

    CUDA 7 Stream流简化并发性 异构计算是指高效地使用系统中的所有处理器,包括 CPU 和 GPU .为此,应用程序必须在多个处理器上并发执行函数. CUDA 应用程序通过在 streams  ...

  2. CUDA 7 流并发性优化

    异构计算是指高效地使用系统中的所有处理器,包括 CPU 和 GPU .为此,应用程序必须在多个处理器上并发执行函数. CUDA 应用程序通过在 streams 中执行异步命令来管理并发性,这些命令是按 ...

  3. 【CUDA 基础】6.0 流和并发

    title: [CUDA 基础]6.0 流和并发 categories: - CUDA - Freshman tags: - 流 - 事件 - 网格级并行 - 同步机制 - NVVP toc: tru ...

  4. CUDA编程接口:异步并发执行的概念和API

    1.主机和设备间异步执行 为了易于使用主机和设备间的异步执行,一些函数是异步的:在设备完全完成任务前,控制已经返回给主机线程了.它们是: 内核发射; 设备间数据拷贝函数; 主机和设备内拷贝小于64KB ...

  5. Cuda Stream流分析

    Cuda Stream流分析 Stream 一般来说,cuda c并行性表现在下面两个层面上: Kernel level Grid level Stream和event简介 Cuda stream是指 ...

  6. Java——IO类,转换流简化写法

    body, table{font-family: 微软雅黑} table{border-collapse: collapse; border: solid gray; border-width: 2p ...

  7. CUDA: 流

    1. 页锁定主机内存 c库函数malloc()分配标准的,可分页(Pagable)的内存,cudaHostAlloc()分配页锁定的主机内存.页锁定内存也称为固定内存(Pinned Memory)或者 ...

  8. CUDA C Best Practices Guide 在线教程学习笔记 Part 1

    0. APOD过程 ● 评估.分析代码运行时间的组成,对瓶颈进行并行化设计.了解需求和约束条件,确定应用程序的加速性能改善的上限. ● 并行化.根据原来的代码,采用一些手段进行并行化,例如使用现有库, ...

  9. Microsoft Orleans构建高并发、分布式的大型应用程序框架

    Microsoft Orleans 在.net用简单方法构建高并发.分布式的大型应用程序框架. 原文:http://dotnet.github.io/orleans/ 在线文档:http://dotn ...

随机推荐

  1. hdu1568斐波那契前4位

    题意:      就是求斐波那契数,但是只要求输出前四位,(n<=100000000). 思路:      这个要用到斐波那契的公式和一些log的规律,直接打看着很乱,直接在网上偷张图片吧:   ...

  2. Fidder抓包软件的使用

    Fiddler是一款强大的Web调试工具,它能记录所有客户端和服务器的HTTP和HTTPS请求.Fiddler是通过改写HTTP代理,让数据从它那通过,来监控并且截取到数据.当然Fiddler很屌,在 ...

  3. DVWA之Brute Force(暴力破解)

    目录 Low Medium High Impossible 暴力破解是指使用穷举法,举出所有的可能的结果,然后逐一验证是否正确! Low 源代码: <?php if( isset( $_GET[ ...

  4. [LeetCode每日一题]81. 搜索旋转排序数组 II

    [LeetCode每日一题]81. 搜索旋转排序数组 II 问题 已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同. 在传递给函数之前,nums 在预先未知的某个下标 k(0 & ...

  5. Java中如何保证线程顺序执行

    只要了解过多线程,我们就知道线程开始的顺序跟执行的顺序是不一样的.如果只是创建三个线程然后执行,最后的执行顺序是不可预期的.这是因为在创建完线程之后,线程执行的开始时间取决于CPU何时分配时间片,线程 ...

  6. Asp.NetCore Web开发之跨域问题

    在前后端分离的web开发中,解决跨域问题是不可避免的,为什么会出现跨域问题呢,这主要是因为web中的"同源策略",浏览器出于安全原因,不让用户随便访问不同于当前站点的资源,也就是说 ...

  7. Educational Codeforces Round 105 (Rated for Div. 2)

    A. ABC String 题目:就是用'('和')'来代替A,B,C并与之对应,问是不是存在这样的对应关系使得'('和')'正好匹配 思路:第一个和最后一个字母是确定的左括号或者是右括号,这样就还剩 ...

  8. 17.继承 and18.接口和多态 内部类 匿名内部类,Lambda表达式

    1. 继承 1.1 继承的实现(掌握) 继承的概念 继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,以及追加属性和方法 实现继承的格式 继承通过extends实现 ...

  9. 认识 Spring Cloud Alibaba

    个人理解 Spring Cloud Alibaba 就是 Spring Cloud 的微服务规范的一种实现,外加一些阿里云的商业组件 Spring Cloud 是什么 Spring Cloud 为开发 ...

  10. Git安装教程最新版本(国内gitee国外github)

    Git安装教程最新版本(国内gitee国外github) 欢迎关注博主公众号「Java大师」, 专注于分享Java领域干货文章, 关注回复「资源」, 获取大师使用的typora主题: http://w ...