GPU 高性能编程 CUDA : 流

对于大规模数据的并行运算,GPU上的执行性能远高于 CPU 上的性能,除此之外, NVIDAI 图像处理器还支持另一种类型的并行性,类似于 CPU 的多线程应用程序中的任务并行性。任务并行性是指并行执行两个或者多个任务,而不是在大量数据上执行同一个任务。

页锁定主页内存

之前都是使用 cudaMalloc() 在 GPU 上分配内存,以及通过标准 C 库函数 malloc() 在主机上分配内存

除此之外,还可以用 cudaHostAlloc() 来分配主机内存

malloc() 分配的是标准的,可分页的主机内存,cudaHostAlloc() 分配的是页锁定主机内存,页锁定主机内存也叫固定内存,有一个重要的属性:操作系统不会对这块内存分页并交到磁盘上,从而确保改内存始终驻留在物理内存中。操作系统可以安全的使某个应用程序访问该内存的物理地址,因为这块内存不会被破坏或者重新定位。

固定内存是把双刃剑,使用固定内存时,将失去虚拟内存的所有功能。特别是,在应用程序中使用每个页锁定内存时都需要分配物理内存,因为这些内存不能交换到磁盘上,这意味着,系统会更快的耗尽内存。

我们建议,仅对 cudaMemcpy() 调用中的源内存或者目标内存,才使用页锁定内存,在不需要使用它们的时候就立即释放,而不是等程序运行结束的时候

页锁定内存可以提高性能,有一些特殊情况也需要使用页锁定内存

CUDA 流

CUDA 流在加速应用方面起到重要的作用。CUDA 流表示一个 GPU 操作队列,并且该队列中的操作符将以指定的顺序执行,我们可以将每一个流视为 GPU 上的一个任务,并且这些任务是可以并行执行的

使用单个 CUDA 流

假设一个核函数,该函数有两个输入数据缓冲区 a 和 b,核函数将对这些缓冲区中相应位置上的值执行某种计算,将生成的结果保存在输出缓冲区 c 

 

#define N (1024*1024)
__global__ void kernel(int *a ,int *b ,int *c){
    int idx = threadIdx.x + blockDim.x * blockIdx.x;
    if(idx < N){
        c[idx] = a[idx] + b[idx];
    }
}

这个核函数不重要,可以把它看成一个抽象函数,重要的是 main() 中和流相关的代码

首先,要选择一个支持设备重叠(device overlap)功能的设备,支持设备重叠功能的 GPU 能够在执行一个 CUDA C 核函数的同时,还能在设备和主机之间执行复制操作

int main(void){
    cudaDeviceProp prop;
    int whichDevice;
    cudaGetDevice(&whichDevice);
    cudaGetDeviceProperties(&prop,whichDevice);
    if(!prop.deviceOverlap){
        printf("Devices will not handle overlaps");
        return 0;
    }
    cudaEvent_t start,stop;
    float elapsedTime;
    //启动计时器
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start,0);
    //初始化流
    cudaStream_t stream;
    cudaSreamCreate(&stream);
    //数据分配操作
    int *host_a, *host_b,*host_c;
    int *dev_a, *dev_b, *dev_c;
    //在 GPU 上分配内存
    cudaMalloc((void**)&dev_a,N*sizeof(int));
    cudaMalloc((void**)&dev_b,N*sizeof(int));
    cudaMalloc((void**)&dev_c,N*sizeof(int));
    //在 CPU 上分配内存,是流使用的页锁定内存
    cudaHostAlloc((void**)&host_a,FULL_DATA_SIZE*sizeof(int),cudaHostAllocDefault);
    cudaHostAlloc((void**)&host_b,FULL_DATA_SIZE*sizeof(int),cudaHostAllocDefault);
    cudaHostAlloc((void**)&host_c,FULL_DATA_SIZE*sizeof(int),cudaHostAllocDefault);
    //随机数填充主机内存
    for(int i =0;i < FULL_DATA_SIZE; i++){
        host_a[i] = rand();
        host_b[i] = rand();
    }
    //在整体数据上循环,每个数据块的大小是 N 
    for(int i = 0 ;i >>(dev_a,dev_v,dev_c);
        //将数据复制到锁定内存中
        cudaMemcpyAsync(host_c+i,dev_c,N*sizeof(int),cudaMemcpyDeviceToHost,stream);
    }
    //同步
    cudaStreamSynchronize(stream);
    //
    cudaEventRecord(stop,0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&elapasedTime,start,stop);
    //释放流和内存
    cudaFreeHost(host_a);
    cudaFreeHost(host_b);
    cudaFreeHost(host_c);
    cudaFree(dev_a);
    cudaFree(dev_b);
    cudaFree(dev_c);
    //销毁流
    cudaStreamDestory(stream);
    return 0;
}

在主机上使用固定内存,调用 cudaHostAlloc() ,使用 cudaHostAlloc() 函数完成页锁定主机内存的分配

执行计算的过程还是将两个输入缓冲区复制到 GPU, 启动核函数,将输入缓冲区复制回主机,只是有一些小的改变

首先不将输入缓冲区整体复制到 GPU ,而是将输入缓冲区划分为更小的块,在每一个块上执行上面的步骤,在 GPU 上内存远小于 CPU  时需要这种操作

和之前的代码有些不同,没有使用 cudaMemcpy() ,而是使用 cudaMemcpyAsync() 在主机和 GPU 之间进行复制数据。cudaMemcpy是同步方式执行,当函数返回的时候,复制操作就已经完成了,并且在缓冲区保存了复制进去的内容,异步函数的行为和同步函数相反,在调用 cudaMemcpyAsync() 时,只是放置了一个请求,表示在流中执行一次内存复制操作,这个流是通过参数 stream 指定的,当函数返回时,我们不知道复制操作是否启动了,更无法保证他结束了,可以保证的是,复制操作肯定会当下一个放入流中的操作之前执行。任何传递给 cudaMemcpyAsync() 的主机内存指针都必须通过 cudaHostAlloc() 分配好内存,即只能以异步的方式对页锁定内存进行复制操作

 流就想一个有序的工作队列,GPU 从该队列中依次取出工作并执行

for 循环结束时,队列中应该有很多等待 GPU 执行的工作,如果想要确保 GPU 执行完了计算和复制等操作,就需要 GPU 和主机同步,可以使用 cudaStreamSynchronize() 并指定想要等待的流

使用多个 CUDA 流

GPU 高性能编程 CUDA : 流_第1张图片

#define N (1024*1024)
#define FULL_DATA_SIZE (N*20)
__global__ void kernel(int *a,int *b,int *c){
    int idx = threadIdx.x + blockIdx.x*blockDim.x;
    if(idx < N){
        c[idx]=a[idx]+b[idx];
    }
}
int main(void){
    cudaDevicesProp prop;
    int whichDevices;
    cudaGetDevices(&whichDevices);
    cudaDeviceProperties(&prop,whichDevices);
    if(!prop.deviceOverlap){
        return 0;
    }
    cudaEcent_t start,stop;
    float elapsdTime;
    //启动计时器
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start,0);
    //初始化流
    cudaStream_t stream0,stream1;
    cudaStreamCreate(&stream0);
    cudaStreamCreate(&stream1);
    //
    int *host_a,*host_b,*host_c;
    int *dev_a0,*dev_b0,*dev_c0;
    int *dev_a1,*dev_b1,*dev_c1;
    //GPU上分配内存
    cudaMalloc((void**)&dev_a0,N*sizeof(int));
    cudaMalloc((void**)&dev_b0,N*sizeof(int));
    cudaMalloc((void**)&dev_c0,N*sizeof(int));
    cudaMalloc((void**)&dev_a1,N*sizeof(int));
    cudaMalloc((void**)&dev_b1,N*sizeof(int));
    cudaMalloc((void**)&dev_c1,N*sizeof(int));
    //分配在流中使用的页锁定内存
    cudaHostAlloc((void**)&host_a,Full_DATA_SIZE*sizeof(int),cudaHostAllocDefault);
    cudaHostAlloc((void**)&host_b,Full_DATA_SIZE*sizeof(int),cudaHostAllocDefault);
    cudaHostAlloc((void**)&host_c,Full_DATA_SIZE*sizeof(int),cudaHostAllocDefault);
    //随机数
    for(int i=0;i>>(dev_a0,dev_b0,dev_c0);
        cudaMemcpyAsync(host_c+i,dev_c0,N*sizeof(int),cudaMemcpyDeviceToHust,stream0);
        //
        cudaMemcpyAsync(dev_a1,host_a+i+N,N*sizeof(int),cudaMemcpyHostToDevice,stream1);
        cudaMemcpyAsync(dev_b1,host_b+i+N,N*sizeof(int),cudaMemcpyHostToDevice,stream1);
        kernel<<>>(dev_a1,dev_b1,dev_c1);
        cudaMemcpyAsync(host_c+i+N,dev_c1,N*sizeof(int),cudaMemcpyDeviceToHust,stream1);
    }
    //
    cudaStreamSynchronize(stream0);
    cudaStreamSynchronize(stream1);
    cudaEventRecord(stop,0);
    cudaEventSynchronize(stop);
    cudaEventSynchronize(&elapasedTime,start,stop);
    //释放内存和流
    cudaFreeHost(host_a);
    cudaFreeHost(host_b);
    cudaFreeHost(host_c);
    cudaFree(dev_a0);
    cudaFree(dev_b0);
    cudaFree(dev_c0);
    cudaFree(dev_a1);
    cudaFree(dev_b1);
    cudaFree(dev_c1);
    cudaStreamDestory(stream0);
    cudaStreamDestory(stream1);
    return 0;
}

使用两块流来处理数据,分配两组相同的 GPU 缓冲区,每一个流可以独立的在数据块上执行工作,由于使用了两个流,在for 循环的迭代中需要处理的数据量也是原来的两倍。for 循环中,交替的把每个数据块放到这两个流的队列,直到所有待处理的输入数据都被放入队列。

GPU 的工作调度机制

流是有序的操作序列,既包含内存复制操作,也包含核函数调用,但是硬件中并没有流的概念,而是包含一个或多个引擎来执行内存复制操作和一个引擎来执行核函数。这些引擎彼此独立的对队列进行排队。

GPU 高性能编程 CUDA : 流_第2张图片

由于第零个流将 c 复制回主机的操作要等到核函数执行完成后,所以第一个流中的将 a 和 b 复制到 GPU 的操作虽然是完全独立的却被阻塞了,这是因为 GPU 引擎是按照指定的顺序来执行的,所以用上面两个流却没有加速。

修改为更高效的代码,只需要改变分配到两个流的顺序,采用宽度优先而不是深度优先方式。就是说,不是先添加第零个流的所有四个操作(即a 的复制,b 的复制,核函数,c 的复制),然后不再添加第一个流的所有四个操作,而是将这两个流的操作交叉添加。首先,将 a 的复制操作加到第零个流,将 a 的复制操作加到第一个流,然后将 b 的复制分别复制到两个流,之后是核函数,在之后复制 c,流程如下

GPU 高性能编程 CUDA : 流_第3张图片

这里第零个流对 c 的复制操作并不会阻碍第一个流对 a,b 的复制操作。这使得 GPU 可以并行的执行复制操作和核函数

你可能感兴趣的:(CUDA)