理论部分

  • 学习使用CUDA Stream和Event
  • 学习使用NVVP工具

技能部分

  • CUDA Stream和Event的使用技巧和经验
  • NVVP的使用技巧和经验

    CUDA Stream

    CUDA stream是GPU上task 的执行队列,所有CUDA操作(kernel,内存拷贝等)都是在stream上执行的。

CUDA stream有两种

  • 隐式流,又叫默认流,NULL流

所有的CUDA操作默认运行在隐式流里。隐式流里的GPU task和CPU端计算是同步的。
举例:𝑛 = 1这行代码,必须等上面三行都执行完,才会执行它。
image.png

  • 显式流:显式申请的流

显式流里的GPU task和CPU端计算是异步的不同显式流内的GPU task执行也是异步的
image.png

CUDA Stream API

  • 定义

    1. cudaStream_t stream;
  • 创建

    cudaStreamCreate(&stream);
    
  • 数据传输

    cudaMemcpyAsync(dst, src, size, type, stream)
    
  • kernel在流中执行

    kernel_name<<<grid, block, sharedMemSize, stream>>>(argument list);
    
  • 同步和查询

    cudaError_t cudaStreamSynchronize(cudaStream_t stream)
    cudaError_t cudaStreamQuery(cudaStream_t stream);
    
  • 销毁

    cudaError_t cudaStreamDestroy(cudaStream_t stream);
    

    CUDA Stream demo

    ```cpp cudaStream_t stream[2]; // 定义两个流 for (int i = 0; i < 2; i++) {

      cudaStreamCreate(&stream[i]);  // 创建两个流
    

    }

    float hostPtr; cudaMallocHost(&hostPtr, 2 size);

    // 两个流,每个流有三个命令 for (int i = 0; i < 2; i++) {

      // 从主机内存复制数据到设备内存
      cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);
      // 再流中执行kernel函数处理
      MyKernel <<< grid, block, 0, stream[i] >>> (outputDevPtr + i * size, inputDevPtr + i * size, size);
      // 从设备内存到主机内存
      cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);
    

    }

    // 同步流 for (int i = 0; i < 2; i++) {

      cudaStreamSynchronize(stream[i]);
    

    }

    // 销毁流 for (int i = 0; i < 2; i++) {

      cudaStreamDestroy(stream[i]);
    

    }

<a name="vo6eO"></a>
### CUDA Stream 优点

- CPU计算和kernel计算并行
- CPU计算和数据传输并行
- 数据传输和kernel计算并行
- kernel计算并行
:::info
**显式流里的GPU task与CPU端 task 的执行是异步的,使用stream一定要注意同步!**

- cudaStreamSynchronize() 同步一个流
- cudaDeviceSynchronize() 同步该设备上的所有
- cudaStreamQuery() 查询一个流任务是否完成
:::
<a name="ODRjI"></a>
### 数据传输和GPU计算重叠
| ![image.png](https://cdn.nlark.com/yuque/0/2022/png/353587/1652019614916-36dde9d5-afd9-4c59-af9e-38a4690ab700.png#clientId=u5809c6a1-1f2d-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=59&id=u2f3b6533&margin=%5Bobject%20Object%5D&name=image.png&originHeight=105&originWidth=646&originalType=binary&ratio=1&rotation=0&showTitle=false&size=35197&status=done&style=none&taskId=u22d959ff-4087-4eca-9777-07d9c383c3d&title=&width=361) | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/353587/1652019624475-d3f1f570-dfcb-4574-8b09-9432871fbfc3.png#clientId=u5809c6a1-1f2d-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=158&id=u34bc4751&margin=%5Bobject%20Object%5D&name=image.png&originHeight=198&originWidth=873&originalType=binary&ratio=1&rotation=0&showTitle=false&size=93562&status=done&style=none&taskId=u87c4daed-1e17-4b8a-be46-db8451b2c12&title=&width=698.4) |
| --- | --- |
| ![image.png](https://cdn.nlark.com/yuque/0/2022/png/353587/1652019636296-1af0ff52-f84e-472b-b547-ffcb5c122e15.png#clientId=u5809c6a1-1f2d-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=209&id=u3cc101c0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=261&originWidth=940&originalType=binary&ratio=1&rotation=0&showTitle=false&size=37088&status=done&style=none&taskId=u3f2523b8-4621-494a-9276-8e11c518468&title=&width=752) |  |

流不是万能的,上述情况只是理论情况,因为流的启动和销毁是耗时的,通常kernel计算特别快,所以当数据量和计算量足够大时,这种提升效果才明显。


:::warning
H2D 、D2H与其他H2D、D2H 为什么没有重叠?它们已经在不同stream上了。
:::
因为CPU和GPU的数据传输是经过PCIe总线的,PCIe上的操作是顺序的。**带有双工PCIe总线的设备**可以重叠两个数据传输,但它们必须在不同的流和不同的方向上。
<a name="BsCEy"></a>
### CUDA Stream 优先级
GPU 算力 3.5 及以上,即Kepler架构及以上,可以为stream设置优先级
```cpp
cudaError_t cudaStreamCreateWithPriority(cudaStream_t* pStream, unsigned int flags, int priority);
cudaError_t cudaDeviceGetStreamPriorityRange(int *leastPriority, int *greatestPriority);
  • stream的优先级只对kernel有效,对内存拷贝无效。
  • flags较低的整数值表示较高的流优先级。

该功能不常用,可暂时忽略

CUDA Stream 为什么有效?

  1. PCIe总线传输速度慢,是瓶颈,会导致传输数据的时候GPU处于空闲等待状态。多流可以实现数据传输与kernel计算的并行。(NV-link可以直接挂在到IBM的power架构上,速度很快,但是一般人没有啊)
  2. 一个kernel往往用不了整个GPU的算力。多流可以让多个kernel同时计算,充分利用GPU算力。

    流越多越好?

    不是流越多越好。GPU内可同时并行执行的流数量是有限的。如果超出了设备的最大线程数,加速效果可能会直线下降。那么如何解决嘞?
    CUDA加速,kernel合并,将小任务合并成大任务,更有效。

计算密集型:耗时在计算,一次访存,数十次甚至上百次计算
访存密集型:耗时在访存,一次访存,几次计算
思考: GPU kernel耗时最大在哪里?GPU一般处理简单可并行计算,大部分kernel都是访存密集型
举例说明此问题:

向量𝐴、𝐵、𝐶,大小都为𝑛。

𝐴 ∗ 𝐵 = 𝐷; 𝐴 ∗ 𝐶 = 𝐸; 𝐸 + 𝐷 = 𝑂;
image.png

但是,计算异常复杂,不鼓励合并大kernel。GPU端的kernel并行思路 + 调试难度远远超过CPU端编程。

CUDA Stream 默认流的表现

https://developer.nvidia.com/blog/gpu-pro-tip-cuda-7-streams-simplify-concurrency/

// nvcc ./stream_test.cu -o stream_legacy
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;
}

期望运行的结果
06. CUDA Stream和Event - 图4
实际运行的结果06. CUDA Stream和Event - 图5
单线程内,默认流的工作方式是同步的,显示流的工作方式是异步的。
单线程内,编译加上--default-stream per-thread 后,默认流的执行是异步的,显式流的执行是异步的,符合预期。并且会将default默认流转换成显示流,才会和其他8个显示流进行异步执行。

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

多线程下,默认流的表现是什么呢?是一个默认流还是多个默认流?

// nvcc ./pthread_test.cu -o pthreads_legacy
#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;
}

正常编译的话,默认多线程共享一个默认流
06. CUDA Stream和Event - 图6
加上--default-stream per-thread之后,编译,每个线程都有一个默认流

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

06. CUDA Stream和Event - 图7

CUDA Event

CUDA Event,在stream中插入一个事件,类似于打一个标记位,用来记录stream是否执行到
当前位置。

stream类似队列,不断的插入task,除了插入内存之间的copy和kernel函数的执行,还能插入Event。

Event有两个状态,已被执行和未被执行。

  • 定义
    cudaEvent_t event
    
    • 创建
    cudaError_t cudaEventCreate(cudaEvent_t* event);
    
    • 插入流中
    cudaError_t cudaEventRecord(cudaEvent_t event, cudaStream_t stream = 0);
    
    • 销毁
    cudaError_t cudaEventDestroy(cudaEvent_t event);
    
    • 同步和查询
    cudaError_t cudaEventSynchronize(cudaEvent_t event);
    cudaError_t cudaEventQuery(cudaEvent_t event);
    
    • 进阶同步函数
    cudaError_t cudaStreamWaitEvent(cudaStream_t stream, cudaEvent_t event)
    

    常用功能-测时间

    ```cpp //使用event计算时间 float time_elapsed = 0; cudaEvent_t start, stop; cudaEventCreate(&start); //创建Event cudaEventCreate(&stop);

cudaEventRecord(start, 0); //记录当前时间 mul<<>>(dev_a, NUM); cudaEventRecord(stop, 0); //记录当前时间

cudaEventSynchronize(start); // waits for an event to complete. cudaEventSynchronize(stop); // waits for an event to complete.Record之前的任务 cudaEventElapsedTime(&time_elapsed , start,stop ); // 计算时间差

cudaEventDestroy(start); // destory the event cudaEventDestroy(stop); printf(“执行时间:%f( ms ) \n”, time_elapsed);

<a name="RlVBM"></a>
## CUDA 同步操作
CUDA中的显式同步按粒度可以分为四类

- device synchronize 影响很大
- stream synchronize 影响单个流和CPU
- event synchronize 影响CPU,更细粒度的同步
- synchronizing across streams **using an event**
<a name="Ud4us"></a>
### device synchronize
![image.png](https://cdn.nlark.com/yuque/0/2022/png/353587/1652175313096-4e7418f7-75c3-4137-860a-eb984745ac4a.png#clientId=ud50b73d3-b6a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=482&id=u4a578025&margin=%5Bobject%20Object%5D&name=image.png&originHeight=603&originWidth=1272&originalType=binary&ratio=1&rotation=0&showTitle=false&size=55889&status=done&style=none&taskId=udb705e4b-c4cb-4862-a390-578c0a1613c&title=&width=1017.6)<br />会让所有的流和cpu都等待。上图中有三个stream,stream1中有两个kernel,顺序执行,stream2和stream3都各自有一个kernel,cpu也在同步计算。当程序执行到短黑色箭头处,加入`cudaDeviceSynchronize`,此时程序中cpu端立马停止,进入等待模式,已经运行的kernel会执行完,没有执行的也会继续等待,直到最长的kernel执行完成。程序才会继续运行。
<a name="Q7lRj"></a>
### stream synchronize
![image.png](https://cdn.nlark.com/yuque/0/2022/png/353587/1652175751273-e29ed597-d5d1-49fb-89d9-dec117a30567.png#clientId=ud50b73d3-b6a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=481&id=u2c2c5d64&margin=%5Bobject%20Object%5D&name=image.png&originHeight=601&originWidth=1266&originalType=binary&ratio=1&rotation=0&showTitle=false&size=62952&status=done&style=none&taskId=u845effc5-734c-4851-a7eb-d63572055f6&title=&width=1012.8)<br />**影响单个流和cpu。**在短黑色箭头处插入对流stream3的同步,stream1和stream2会正常执行,cpu会立即处于等待状态,当stream3完成之后,cpu也会恢复状态、
<a name="nRqIw"></a>
### event synchronize 影响CPU,更细粒度的同步
![image.png](https://cdn.nlark.com/yuque/0/2022/png/353587/1652175798450-6a682815-107c-4151-a343-ba1c1b9f05a0.png#clientId=ud50b73d3-b6a5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=306&id=u1f6d07b7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=382&originWidth=1326&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31411&status=done&style=none&taskId=u39181a99-efb0-42de-be31-d1e1630416a&title=&width=1060.8)<br />将event1插入到stream中,当运行到cudaEventSynchronize时,cpu会立即进入等待状态,等待event1被标记为完成,cpu才会继续运行。
<a name="Bdtv2"></a>
### synchronizing across streams using an event
以上三种同步,CPU一定会处于等待状态,该API可以使CPU继续执行
```cpp
cudaError_t cudaStreamWaitEvent(cudaStream_t stream, cudaEvent_t event);

该函数会指定该stream等待特定的event,该event可以关联到相同或者不同的stream流中。
image.png
stream1和stream中有很多task在运行,当在stream1中插入event,当在stream2中运行cudaStreamWaitEvent,event是在stream1中插入的event。stream2中之后的task会等待event1完成之后,才会继续执行。
这种同步方式,高级用法,不建议设计流时这么复杂,平常用不到