CUDA C编程入门-编程接口(3.2)CUDA C运行时

  在cudart库里实现了CUDA C运行时,应用可以链接静态库cudart.lib或者libcudart.a,动态库cudart.dll或者libcudart.so。动态链接cudart.dll或者libcudart.so的应用需要把CUDA的动态链接库(cudart.dll或者libcudart.so)包含到应用的安装包里。

  CUDA所有的运行时函数都以cuda为前缀。

  在异构编程这一章节提到,CUDA编程模型假设系统是由一个自带各自的内存的主机和设备组成。设备内存这一小节概述用于管理设备内存的运行时函数。

  共享内存这一小节说明在线程层次中提到的共享内存的用法以达到最大化性能。

  Page-Locked主机内存这节介绍page-locked内存,要求与核函数执行在主机和设备之间数据交换时同时发生。

  异步并行执行这节描述系统中在不同级别使用的异步并行执行的概念和API。

  多设备系统这节展示编程模型怎样扩展同样一个主机连接多个设备的系统。

  错误检查这节描述怎么合适地检查运行时产生的错误。

  调用堆栈这节提到用于管理CUDA C调用堆栈的运行时函数。

  纹理和曲面内存这节显示提供另外的访问设备内存的方法的纹理和曲面内存,同时它们也显示GPU纹理硬件的子集。

  图形互操作性介绍各种提供与两种主要的图形API-OpenGL和Direct3D的交互的运行时函数。

3.2.1 初始化

  没有明确的运行时的初始化函数。运行时函数(更具体地说,除了设备的和参考手册的版本控制章节的函数)初次调用时会初始化。在运行运行时时,一点需要记住的是定时的运行时和解释错误代码的函数会被调用。

  在初始化期间,运行时为系统中的每个设备建立一个CUDA上下文(上下文这节有关于CUDA上下文的描述)。这个是设备primary上下文,被应用的所有主机线程共享。作为建立上下文的一部分,设备代码需要时会即时编译和加载进设备内存。这个所有在高级选项下发生,且运行时没有暴露主要的上下文给应用。

  当一个主机线程调用cudaDeviceReset()销毁主机当前操作的设备的主要上下文。任何当前拥有这个设备的主机线程调用运行时函数时将会为这个设备建立一个新的主要的上下文。

3.2.2 设备内存

  在异构编程中提到的,CUDA编程模型假设系统是由有自己独立内存的主机和设备组成的。核函数操作设备内存,因此运行时提供分配、回收和在主机和设备内存直接拷贝数据的函数。

  设备内存能被分配成线性的内存或者CUDA数组。

  CUDA数组是为纹理fetching优化的不透明的内存布局。在纹理和曲面内存这小节描述。

  线性的内存存在于计算能力为1.x的、32位地址空间的设备,更高计算能力为40位地址空间,所以可以通过指针各自地引用分配的空间,例如在一棵二叉树中。一般使用cudaMalloc()分配线性的空间,cudaFree()回收内存空间,cudaMemcpy()在主机和设备内存中拷贝数据。在向量加的核函数的代码例子中,需要在主机和设备之间拷贝向量。

  

#include "cuda_runtime.h"
#include "device_launch_parameters.h"

#include <stdio.h>
#include <stdlib.h>

// Device Code
__global__ void VecAdd(float *A, float *B, float *C, int N)
{
    int i = blockDim.x * blockIdx.x + threadIdx.x;
    if(i < N)
    {
        C[i] = A[i] + B[i];
    }
}

// Host Code
int main()
{
    int N = 10;
    size_t size = N * sizeof(float);

    //allocate input vectors in host memory
    float *host_A = (float*)malloc(size);
    float *host_B = (float*)malloc(size);
    float *host_C = (float*)malloc(size);

    for(int i = 0; i < N; ++i)
    {
        host_A[i] = i;
        host_B[i] = i;
        host_C[i] = 0;
    }

    //printf A
    printf("A:");
    for(int i = 0; i < N; ++i)
    {
        printf(" %.2f", host_A[i]);
    }
    printf("\n");

    //printf B
    printf("B:");
    for(int i = 0; i < N; ++i)
    {
        printf(" %.2f", host_B[i]);
    }
    printf("\n");

    //printf C
    printf("C:");
    for(int i = 0; i < N; ++i)
    {
        printf(" %.2f", host_C[i]);
    }
    printf("\n");
    //allocate vectors in device memory
    float *dev_A;
    cudaMalloc(&dev_A, size);
    float *dev_B;
    cudaMalloc(&dev_B, size);
    float *dev_C;
    cudaMalloc(&dev_C, size);

    //copy vectors from host memory to device memory
    cudaMemcpy(dev_A, host_A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(dev_B, host_B, size, cudaMemcpyHostToDevice);

    //invoke hernel
    int threadsPerBlock = 256;
    //因为N可能不能被threadsPerBlock整除,
    //如果能被整除的话,blocksPerGrid 可以设为N/threadsPerBlock,
    //如果不能整除,则N除threadsPerBlock余数可以为1到threadsPerBlock-1之间的数(包括端点1和threadsPerBlock-1)
    //那么要使blocksPerGrid*threadsPerBlock>=N,则可以设blocksPerGrid为(N + threadsPerBlock - 1) / threadsPerBlock
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    VecAdd<<<blocksPerGrid, threadsPerBlock>>>(dev_A, dev_B, dev_C, N);

    //copy result from device memory to hosy memory
    cudaMemcpy(host_C, dev_C, size, cudaMemcpyDeviceToHost);

    //printf C
    printf("C:");
    for(int i = 0; i < N; ++i)
    {
        printf(" %.2f", host_C[i]);
    }
    printf("\n");

    //free device memory
    cudaFree(dev_A);
    cudaFree(dev_B);
    cudaFree(dev_C);

    //free host memory
    free(host_A);
    free(host_B);
    free(host_C);

    return 0;
}

  线性内存也能通过cudaMallocPitch()和cudaMalloc3D()函数分配。这些函数一般用于分配2维和3维的数组,并且适当地填充以满足在设备内存访问这一小节描述的对齐要求,所以保证访问行地址或者在2维数组和其它区域的设备内存之间拷贝数据(使用cudaMemcpy2D()和cudaMemcpy3D()函数)达到最好的性能。返回的pitch(或者stride)必须用于访问数组元素。下面代码是分配一个width*height的2维的浮点数值数组,并演示如何在设备代码中遍历数组元素:

// Host code
int width = 64, height = 64;
float* devPtr;
size_t pitch;
cudaMallocPitch(&devPtr, &pitch,
width * sizeof(float), height);
MyKernel<<<100, 512>>>(devPtr, pitch, width, height);
// Device code
__global__ void MyKernel(float* devPtr,
size_t pitch, int width, int height)
{
for (int r = 0; r < height; ++r) {
    float* row = (float*)((char*)devPtr + r * pitch);
        for (int c = 0; c < width; ++c) {
            float element = row[c];
        }
    }
}

  下面的代码分配一个width*height*depth的3维的浮点数值数组,并演示如何在设备代码中遍历数组元素:

// Host code
int width = 64, height = 64, depth = 64;
cudaExtent extent = make_cudaExtent(width * sizeof(float),
height, depth);
cudaPitchedPtr devPitchedPtr;
cudaMalloc3D(&devPitchedPtr, extent);
MyKernel<<<100, 512>>>(devPitchedPtr, width, height, depth);
// Device code
__global__ void MyKernel(cudaPitchedPtr devPitchedPtr,
int width, int height, int depth)
{
    char* devPtr = devPitchedPtr.ptr;
    size_t pitch = devPitchedPtr.pitch;
    size_t slicePitch = pitch * height;
    for (int z = 0; z < depth; ++z) {
        char* slice = devPtr + z * slicePitch;
        for (int y = 0; y < height; ++y) {
            float* row = (float*)(slice + y * pitch);
            for (int x = 0; x < width; ++x) {
                float element = row[x];
            }
        }
    }
}

 

  参考手册列出了所有各种用于使用cudaMalloc分配的线性内存、使用cudaMallocPitch或者cudaMalloc3D分配的线性内存、CUDA数组和在全局和常量内存空间分配的变量拷贝内存的函数。

  下面的代码展示各种通过运行时的API访问全局变量:

__constant__ float constData[256];
float data[256];
cudaMemcpyToSymbol(constData, data, sizeof(data));
cudaMemcpyFromSymbol(data, constData, sizeof(data));
__device__ float devData;
float value = 3.14f;
cudaMemcpyToSymbol(devData, &value, sizeof(float));
__device__ float* devPointer;
float* ptr;
cudaMalloc(&ptr, 256 * sizeof(float));
cudaMemcpyToSymbol(devPointer, &ptr, sizeof(ptr));

  cudaGetSymbolAddress()用于获得在全局内存空间分配的变量的地址指针。分配的内存的大小可以通过cudaGetSymbolSize()函数获得。

3.2.3 共享内存

  在变量类型限定符这节中,使用__shared__限定符分配共享内存。

  线程层次这这节提到共享内存访问速度比全局内存快,细节描述在共享内存这节。以下面的矩阵乘例子说明,任何有机会使用共享内存访问替代全局内存就应该利用。

  下面的代码例子只是简单地实现矩阵乘,并为利用共享内存的优势。如图9所示,每个线程读取一行A和一列B,计算相应的元素C。因此从全局内存读取B.width次的A和A.height次的B(应该指的是每一行的A重复读取B.width次和每一列B重复读取A.height次)。

// Matrices are stored in row-major order:
// M(row, col) = *(M.elements + row * M.width + col)
typedef struct {
    int width;
    int height;
    float* elements;
} Matrix;
// Thread block size
#define BLOCK_SIZE 16
// Forward declaration of the matrix multiplication kernel
__global__ void MatMulKernel(const Matrix, const Matrix, Matrix);
// Matrix multiplication - Host code
// Matrix dimensions are assumed to be multiples of BLOCK_SIZE
void MatMul(const Matrix A, const Matrix B, Matrix C)
{
    // Load A and B to device memory
    Matrix d_A;
    d_A.width = A.width; d_A.height = A.height;
    size_t size = A.width * A.height * sizeof(float);
    cudaMalloc(&d_A.elements, size);
    cudaMemcpy(d_A.elements, A.elements, size,cudaMemcpyHostToDevice);
    Matrix d_B;
    d_B.width = B.width; d_B.height = B.height;
    size = B.width * B.height * sizeof(float);
    cudaMalloc(&d_B.elements, size);
    cudaMemcpy(d_B.elements, B.elements, size,cudaMemcpyHostToDevice);
    // Allocate C in device memory
    Matrix d_C;
    d_C.width = C.width; d_C.height = C.height;
    size = C.width * C.height * sizeof(float);
    cudaMalloc(&d_C.elements, size);
    // Invoke kernel
    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
    dim3 dimGrid(B.width / dimBlock.x, A.height / dimBlock.y);
    MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);
    // Read C from device memory
    cudaMemcpy(C.elements, Cd.elements, size,
    cudaMemcpyDeviceToHost);
    // Free device memory
    cudaFree(d_A.elements);
    cudaFree(d_B.elements);
    cudaFree(d_C.elements);
}
// Matrix multiplication kernel called by MatMul()
__global__ void MatMulKernel(Matrix A, Matrix B, Matrix C)
{
    // Each thread computes one element of C
    // by accumulating results into Cvalue
    float Cvalue = 0;
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    for (int e = 0; e < A.width; ++e)
        Cvalue += A.elements[row * A.width + e] * B.elements[e * B.width + col];
    C.elements[row * C.width + col] = Cvalue;
}

CUDA C编程入门-编程接口(3.2)CUDA C运行时_第1张图片

图9 不使用共享内存的矩阵乘

  下面的代码例子是利用共享内存的优势的矩阵乘实现。实现中,每个线程block只计算C的一个子矩阵Csub,block中的每个线程计算Csub的每个元素。如图10所示,Csub矩阵等于两个矩形矩阵的乘积:维数为(A.width,block_size)的A的子矩阵与Csub有一样的行下标,维数为(block_size,A.width)的B的子矩阵有于Csub一样的列下标。为了适配设备的资源,两个矩形矩阵被分为许多维数为block_size的方块矩阵,Csub计算为这些方块矩阵的乘积的和。每个积在第一次计算的时候会从全局内存加载两个相应的方块矩阵到共享内存中,每个线程加载每个矩阵的一个元素,并计算乘积。每个线程把每次的积累加进一个寄存器,最后把结果写入全局内存。

  通过这样的方块计算,由于只需从全局内存中读取(B.width/block_size)次的A和(A.height/block_size)次的B,我们利用快速的共享内存访问的优势并节约许多的全局内存的带宽。

  向前面的代码中的矩阵类型中增加一个stride字段,因此子矩阵可以有效的表示为同样的类型。__device__函数用于获取、设置和建立矩阵的子矩阵。

// Matrices are stored in row-major order: 
// M(row, col) = *(M.elements + row * M.stride + col) 
typedef struct { 
    int width; 
    int height; 
    int stride; 
    float* elements; 
} Matrix; 
// Get a matrix element 
__device__ float GetElement(const Matrix A, int row, int col) {     
    return A.elements[row * A.stride + col]; 
} 

// Set a matrix element 
__device__ void SetElement(Matrix A, int row, int col, float value) { 
    A.elements[row * A.stride + col] = value; 
} 
// Get the BLOCK_SIZExBLOCK_SIZE sub-matrix Asub of A that is 
// located col sub-matrices to the right and row sub-matrices down
 // from the upper-left corner of A 
__device__ Matrix GetSubMatrix(Matrix A, int row, int col) { 
    Matrix Asub; 
    Asub.width = BLOCK_SIZE; 
    Asub.height = BLOCK_SIZE; 
    Asub.stride = A.stride; 
    Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row + BLOCK_SIZE * col]; 
    return Asub; 
} 
// Thread block size 
#define BLOCK_SIZE 16 
// Forward declaration of the matrix multiplication kernel 
__global__ void MatMulKernel(const Matrix, const Matrix, Matrix); 
// Matrix multiplication - Host code 
// Matrix dimensions are assumed to be multiples of BLOCK_SIZE 
void MatMul(const Matrix A, const Matrix B, Matrix C) { 
// Load A and B to device memory 
    Matrix d_A; 
    d_A.width = d_A.stride = A.width; 
    d_A.height = A.height; 
    size_t size = A.width * A.height * sizeof(float); 
    cudaMalloc(&d_A.elements, size); 
    cudaMemcpy(d_A.elements, A.elements, size,          
    cudaMemcpyHostToDevice); 
    Matrix d_B; 
    d_B.width = d_B.stride = B.width; 
    d_B.height = B.height; 
    size = B.width * B.height * sizeof(float); 
    cudaMalloc(&d_B.elements, size); 
    cudaMemcpy(d_B.elements, B.elements, size,     
    cudaMemcpyHostToDevice); 
    // Allocate C in device memory 
    Matrix d_C; 
    d_C.width = d_C.stride = C.width; 
    d_C.height = C.height; 
    size = C.width * C.height * sizeof(float); 
    cudaMalloc(&d_C.elements, size); 
    // Invoke kernel 
    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE); 
    dim3 dimGrid(B.width / dimBlock.x, A.height / dimBlock.y); 
    MatMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C); 
    // Read C from device memory 
    cudaMemcpy(C.elements, d_C.elements, size,     
    cudaMemcpyDeviceToHost); 
    // Free device memory 
    cudaFree(d_A.elements); 
    cudaFree(d_B.elements); 
    cudaFree(d_C.elements); 
} 
// Matrix multiplication kernel called by MatMul() 
__global__ void MatMulKernel(Matrix A, Matrix B, Matrix C) { 
    // Block row and column 
    int blockRow = blockIdx.y; 
    int blockCol = blockIdx.x; 
    // Each thread block computes one sub-matrix Csub of C 
    Matrix Csub = GetSubMatrix(C, blockRow, blockCol); 
    // Each thread computes one element of Csub 
    // by accumulating results into Cvalue 
    float Cvalue = 0; 
    // Thread row and column within Csub 
    int row = threadIdx.y; 
    int col = threadIdx.x; 
    // Loop over all the sub-matrices of A and B that are 
    // required to compute Csub 
    // Multiply each pair of sub-matrices together 
    // and accumulate the results 
    for (int m = 0; m < (A.width / BLOCK_SIZE); ++m) { 
        // Get sub-matrix Asub of A 
        Matrix Asub = GetSubMatrix(A, blockRow, m); 
        // Get sub-matrix Bsub of B 
        Matrix Bsub = GetSubMatrix(B, m, blockCol); 
        // Shared memory used to store Asub and Bsub respectively 
        __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; 
        __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE]; 
        // Load Asub and Bsub from device memory to shared memory 
        // Each thread loads one element of each sub-matrix 
        As[row][col] = GetElement(Asub, row, col); 
        Bs[row][col] = GetElement(Bsub, row, col); 
        // Synchronize to make sure the sub-matrices are loaded 
        // before starting the computation 
        __syncthreads(); 
        // Multiply Asub and Bsub together 
        for (int e = 0; e < BLOCK_SIZE; ++e) 
            Cvalue += As[row][e] * Bs[e][col]; 
        // Synchronize to make sure that the preceding 
        // computation is done before loading two new
        __syncthreads(); 
    } 
    // Write Csub to device memory 
    // Each thread writes one element 
    SetElement(Csub, row, col, Cvalue); 
}    

CUDA C编程入门-编程接口(3.2)CUDA C运行时_第2张图片

图10 使用共享内存的矩阵乘

3.2.4 Page-Locked主机内存

  运行时提供允许用户page-locked的主机内存的函数(与之相对的是传统的使用malloc()分配的可调页的主机内存)。

  • cudaHostAlloc()和cudaFreeHost()分配和回收page-locked主机内存
  • cudaHostRegister() page-locked一段由malloc()分配的内存(参考手册中查看局限性)。

  使用page-locked内存有几点好处:

  • 在page-locked主机内存和设备内存直接拷贝数据能与在异步并行执行这节提到的某些设备的核执行体一同执行。
  • 某些设备上,page-locked主机内存能够映射到设备的内存的地址空间。消除映射内存这节提到的需要拷贝诸主机数据到设备或者从设备拷贝数据到主机。
  • 在系统的前端总线上,如果主机分配的是page-locked内存,主机和设备间的带宽将更高,如果额外分配成在写合并内存这节描述的写合并内存,带宽将达到更高。

  然而,page-locked是一个稀缺的资源,所以在分配可调页的内存之前,分配page-locked内存开始将失败。另外,会减少操作系统的物理内存的分页数量,消耗太多的page-locked内存会降低系统整体的性能。

  在page-locked内存API详细的文档中,有一个简单的零拷贝的CUDA例子。

3.2.4.1 Portable内存

  一块page-locked内存能被系统中的所有设备所共享,但是,默认地,上面描述的使用page-locked内存的好处只能被已经块分配的那些共享设备利用的(在统一的虚拟地址空间这节描述的,如果有的话,所有的设备共享相同的地址空间)。需要分配的块通过从cudaHostAllocPortable、cudaHostAlloc()或者page-locked的从cudaHostRegisterPortable传递标识到cudaHostRegister()函数才能利用这个对所有设备可用的优势。

3.2.4.2 Write-Combining内存

  默认下,分配的page-locked主机内存是可缓存的。通过传递cudaHostAllocWriteCombined标识给cudaHostAlloc(),可以分配成写合并的内存。写合并腾出主机的L1和L2缓存资源,给剩下的应用留下更多可用的缓存。另外,传输通过PCI Express总线时,写合并内存不会被检查,这样可以使传输性能提高40%。从主机的写合并内存读取数据的速度会很慢,所以写合并一般用于主机写。

3.2.4.3 映射内存

  在计算能力大于1.0以上的设备上,通过传递cudaHostAllocMapped标识给cudaHostAlloc()或者cudaHostRegisterMapped给cudaHostRegister(),page-locked主机内存块可以映射到设备内存的地址空间。因此这一的块一般拥有两个地址:一个由cudaHostAlloc()或者malloc()返回的主机内存,另一个为使用cudaHostGetDevicePointer()获得的设备内存,然后被用于在核函数中访问内存块。当主机和设备使用统一地址空间(统一虚拟地址空间这节提到)时,cudaHostAlloc()分配的指针是一个例外。

  在核函数中直接访问主机内存的有几点优势:

  • 不需要在设备中分配内存块、在主机和设备之间拷贝数据、数据传输会在核函数需要时隐式的执行。、
  • 不需要使用流在核执行与数据传输同时发生;源自核函数的数据传输会自动地与核函数执行同时发生。

由于映射的page-locked内存被主机和设备共享,所以应用需要使用流或者事件(查看异步并行执行这节)同步数据访问去避免任何潜在的写之后读、读之后写或者写之后写的危害。

  为了获取指向映射的page-locked内存的指针,在任何其他的CUDA调用之前,page-locked内存映射需要通过传递cudaDeviceMapHost标识调用cudaSetDeviceFlags()启用。否则,cudaHostGetDevicePointer()会返回错误。

  cudaHostGetDevicePointer()也会在设备不支持映射page-locked主机内存时返回错误。应用应该需要通过检测设备canMapHostMemory属性查询是有这个能力,支持映射page-locked主机内存的设备,canMapHostMemory属性为1。

  注意:从主机或者设备的角度看,在映射的page-locked内存上的原子函数操作不是原子的。

3.2.5 异步并行执行

3.2.5.1 在主机和设备之间并行执行

  为了方便在主机和设备之间并行执行,某些函数的调用是异步的:在设备完成任务之前,控制返回至主机线程。它们是:

  • 调用核函数
  • 同一设备内存的两个地址之间的内存拷贝
  • 主机和设备之间的小于或者等于64KB的内存的拷贝
  • 以Async后缀的函数执行的内存拷贝
  • 调用内存设置的函数

  程序员可以通过设置CUDA_LAUNCH_BLOCKING环境变量为1使得在系统运行的所有的CUDA应用禁止异步地执行核函数。这个特性只是提供用于调试而不是用于作为一个方法使软件产品可靠地运行。

在下列情况下,运行核函数是同步的:

  • 在计算能力为1.x的设备上,应用通过一个调试器(cuda-gdb)或者内存检测器(cuda-memcheck,Nsight)运行的。
  • 通过分析工具(Nsight,Visual Profiler)收集硬件计数

3.2.5.2 数据传输与核函数执行同时发生

  计算能力为1.x或者更高的某些设备能与核函数执行时同时在page-locked主机内存和设备内存之间执行拷贝数据。应用可以通过检测asyncEngineCount设备属性查询是否具备这个能力,大于0表示这个设备支持这种能力。计算能力为1.x的设备仅支持不是通过cudaMallocPitch()分配的CUDA数组和2维数组的内存拷贝。

3.2.5.3 同时发生的核函数执行

  计算能力为2.x或者更高的设备可以同时执行多个核函数。应用可以通过检测concurrentKernels设备属性来查询是否有这个能力,等于1时设备支持这样的能力。

  在计算能力为3.5的设备上,设备能同时执行的核函数最大个数为32,而低的的为16。

  一个CUDA上下文的核函数不能与另外的CUDA上下文的核函数同时执行。

  使用许多纹理的或者大量的局部内存的核函数较少可能同时与其它的核函数同时执行。

3.2.5.4 同时发生的数据传输

  计算能力为2.x或者更高的设备能同时执行从page-locked主机内存到设备内存和从设备内存到page-locked主机内存数据拷贝。应用可以检测asyncEngineCount设备属性来查询是否有这个能力,等于2时设备支持这样的能力。

3.2.5.5 流

  应用可以通过流管理并行。流就是按次序执行的一序列命令(可能由不同主机的线程提交的)。另一方面,不同的流可以以相对次序或者并行地执行命令;这样的行为没有保障,也不能依赖于这样的行为来保持准确性(内部的核函数之间的通信是未定义的)。

3.2.5.5.1 创建和销毁

  通过创建流对象来定义流,在调用核函数或者内存拷贝函数时,传递流作为参数。下面的代码创建两个流对象,分配float类型的page-locked内存hostPtr。

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]);
    MyKernel <<<100, 512, 0, stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);
    cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);
}

  每个流拷贝输入数组hostPtr的一部分数据到设备内存的数组inputDevPtr中,调用MyKernel()核函数处理inputDevPtr数组里的数据,然后把核函数的结果outputDevPtr的数据拷贝到hostPtr中去。同时发生的行为这节描述流依靠设备的能力怎样在这样的例子中同时发生。注意,hostPtr必须指向page-locked主机内存,这样核函数调用和数据拷贝才能同时发生。

  调用cudaStreamDestroy()函数销毁流对象。

for (int i = 0; i < 2; ++i)
    cudaStreamDestroy(stream[i]);

  cudaStreamDestroy()在销毁流对象之前会等待给定流完成先前的命令,并把控制返回给主机线程。

3.2.5.5.2 默认的流

  核函数调用和主机-设备的内存拷贝不会指定流参数,或者相当于设置默认流参数为0。因此它们(核函数、内存拷贝)按次序执行。

3.2.5.5.3 显式同步

  有很多的方法可以显式同步流和流。

  cudaDeviceSynchronize()等待主机所有的线程的所有流执行完命令。

  cudaStreamSynchronize()传递一个流作为参数,等待给定流的命令完成。用于同步主机和指定的流,允许其它的流继续在设备上执行。

  cudaStreamWaitEvent()传递一个流和事件作为参数,调用cudaStreamWaitEvent()延迟流所有的命令执行直到传递的事件发生。这个流可以为0,这样情况下,调用cudaStreamWaitEvent()后,添加到所有的流的命令会等待事件发生。

  cudaStreamQuery()提供应用查询一个流的所有的命令是否执行完成。

  为了避免不必要的运行的速度降低,所有同步函数一般最好用于定时,或者不能阻止核函数调用或者内存拷贝。

3.2.5.5.4 隐式同步

  来自不同流的两个命令,如果在下面的两个命令之间的任何一个被主机线程调用的操作发生,不能同时运行:

  • 分配page-locked主机内存
  • 分配设备内存
  • 设置设备内存
  • 同一设备内存的不同地址的之间的内存拷贝
  • 默认流的任何CUDA命令
  • L1和共享内存的配置的转换

  计算能力为3.0或者以下的、支持并发的核函数执行的设备,任何操作需要检查一个流的核函数调用是否执行完成:

  • 在CUDA上下文的任何流的先前调用的核函数的所有线程块开始执行时,任何操作才可以开始执行。
  • 在核函数调用完成之后,CUDA上下文中的任何流的后面的核函数才能调用。

  需要独立检测的操作包括检测在同一流的任何其它的命令是否执行和任何在流上调用cudaStreamQuery()。因此,应用需要遵循下面的指导改进潜在的核函数并行:

  • 在不是独立的操作之前需要调用所有独立的操作。
  • 任何类型的同步应该越晚调用越好。

3.2.5.5.5 同时发生的行为

  无论设备是否支持数据拷贝与核函数、核函数与核函数和数据拷贝与数据拷贝同时执行,两个之间的同时执行的核函数数量取决于提交给每个流的命令的次序。

  比如,在不支持数据拷贝和数据拷贝同时发生,创建和销毁这节的代码中两个流根本不会同时执行,因为提交给stream[1]的主机到设备的内存拷贝,在之后,提交给stream[0]的设备到主机的内存拷贝,stream[1]这个流只会在stream[0]执行完毕之后再执行。如果代码写成下面的方式(假设设备支持数据拷贝和核函数同时执行):

for (int i = 0; i < 2; ++i)
    cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);
for (int i = 0; i < 2; ++i)
    MyKernel<<<100, 512, 0, stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);
for (int i = 0; i < 2; ++i)
    cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);

  这样,stream[0]和stream[1]会同时执行。

  在支持数据拷贝和数据拷贝同时执行的设备上,即使把核函数调用提交给stream[0](设备支持核函数和数据拷贝同时执行),创建和销毁这节的代码中两个流会也同时执行。然而,计算能力为3.0或者以下的设备,核函数不会同时执行。如果代码写成上面的样子,核函数会同时执行。

3.2.5.5.6 回调函数

  运行时提供一个cudaStreamAddCallback()函数可以在流中任何地方插入一个回调函数。回调函数是在主机执行的。

  下面的代码示例添加一个叫MyCallback的回调函数到每个流中:

void CUDART_CB MyCallback(cudaStream_t stream, cudaError_t status, void *data){
    printf("Inside callback %d\n", (size_t)data);
}
...
for (size_t i = 0; i < 2; ++i) {
    cudaMemcpyAsync(devPtrIn[i], hostPtr[i], size, cudaMemcpyHostToDevice, stream[i]);
    MyKernel<<<100, 512, 0, stream[i]>>>(devPtrOut[i], devPtrIn[i], size);
    cudaMemcpyAsync(hostPtr[i], devPtrOut[i], size, cudaMemcpyDeviceToHost, stream[i]);
    cudaStreamAddCallback(stream[i], MyCallback, (void*)i, 0);
}

  任何在回调函数之后提交给流命令只会在回调函数执行完成之后才能执行。cudaStreamAddCallback()的最后一个参数保留。

  回调函数不能调用CUDA API(间接地或者直接地),否则可能会陷入死锁。

3.2.5.5.7 流的优先级

  使用cudaStreamCreateWithPriority()在创建流指定优先级。允许的优先级范围为[ highest priority, lowest priority ],可以通过cudaDeviceGetStreamPriorityRange()获得。运行时,低优先级的会等待高优先级的blocks。

  下面的代码获得当前设备的允许的优先级范围,并创建可用的高优先级和低优先级的流:

// get the range of stream priorities for this device
int priority_high, priority_low;
cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high);
// create streams with highest and lowest available priorities
cudaStream_t st_high, st_low;
cudaStreamCreateWithPriority(&st_high, cudaStreamNonBlocking, &priority_high);
cudaStreamCreateWithPriority(&st_low, cudaStreamNonBlocking, &priority_low);

3.2.5.6 事件

  运行时同时也通过监视设备运行的方法,比如执行精确的时间,让应用在程序的任何点异步地标记事件,查询事件什么时候完成。事件当所有的任务、指定的、给定流的所有命令完成后完成。在流0的事件会在先前的所有任务和所有流的命令执行完成之后完成。

3.2.5.6.1 创建和销毁

  创建事件:

cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);

  销毁事件:

cudaEventDestroy(start);
cudaEventDestroy(stop);

3.2.5.6.2 运行时间

  用于计时的代码例子:

cudaEventRecord(start, 0);
for (int i = 0; i < 2; ++i) {
    cudaMemcpyAsync(inputDev + i * size, inputHost + i * size, size, cudaMemcpyHostToDevice, stream[i]);
    MyKernel<<<100, 512, 0, stream[i]>>>(outputDev + i * size, inputDev + i * size, size);
    cudaMemcpyAsync(outputHost + i * size, outputDev + i * size, size, cudaMemcpyDeviceToHost, stream[i]);
}
cudaEventRecord(stop, 0);
cudaEventSynchronize(stop);
float elapsedTime;
cudaEventElapsedTime(&elapsedTime, start, stop);

3.2.5.7 同步调用

  当一个同步的函数被调用时,在设备执行好任务之前控制不会返回到主机线程。在任何被主机线程调用的CUDA调用执行之前,无论主机线程将会yield(产生)、block(阻塞)或者spin(不知道怎么翻译),可以通过传递一些特定的标识调用cudaSetDeviceFlags()函数。

3.2.6 多设备系统

3.2.6.1 设备枚举

  一个主机可以有多个设备,下面的代码显示怎样枚举这些设备,查询属性和确定支持CUDA的设备数目。

int deviceCount;
cudaGetDeviceCount(&deviceCount);
int device;
for (device = 0; device < deviceCount; ++device) {
    cudaDeviceProp deviceProp;
    cudaGetDeviceProperties(&deviceProp, device);
    printf("Device %d has compute capability %d.%d.\n", device, deviceProp.major, deviceProp.minor);
}

3.2.6.2 设备选择

  主机可以在任何时候通过调用cudaSetDevice()函数设置当前操作的设备。设备内存分配和核函数调用作用于当前设置的设备。在当前相关的设备中创建流和事件。如果没有调用cudaSetDevice()函数,当前的设备默认为设备0。

  下面的代码演示怎么样设置当前设备用于内存分配和执行核函数。

size_t size = 1024 * sizeof(float);
cudaSetDevice(0); // Set device 0 as current
float* p0;
cudaMalloc(&p0, size); // Allocate memory on device 0
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
float* p1;
cudaMalloc(&p1, size); // Allocate memory on device 1
MyKernel<<<1000, 128>>>(p1); // Launch kernel on device 1

3.2.6.3 流和事件的行为

  下面的代码说明提交核函数给流没有与之相关的设备,核函数执行会失败。

cudaSetDevice(0); // Set device 0 as current
cudaStream_t s0;
cudaStreamCreate(&s0); // Create stream s0 on device 0
MyKernel<<<100, 64, 0, s0>>>(); // Launch kernel on device 0 in s0
cudaSetDevice(1); // Set device 1 as current
cudaStream_t s1;
cudaStreamCreate(&s1); // Create stream s1 on device 1
MyKernel<<<100, 64, 0, s1>>>(); // Launch kernel on device 1 in s1
// This kernel launch will fail:
MyKernel<<<100, 64, 0, s0>>>(); // Launch kernel on device 1 in s0

  提交没有与之相关的设备的流的内存拷贝,执行会成功。

  当事件和输入的流不相关,cudaEventRecord()执行会失败。

  当输入的两个事件关联不同的设备,cudaEventElapsedTime()会执行会失败。

  当前的设备与事件关联的设备不同,cudaEventSynchronize()和cudaEventQuery()也会执行成功。

  输入流和事件关联到不同的设备,cudaStreamWaitEvent()也会执行成功。cudaStreamWaitEvent()能用于同步多个设备。

  每个设备有自己默认的流,提交给设备的默认流的命令可能乱序或者同时地与提交给任何其它的设备的默认流的命令执行。

3.2.6.4 点对点内存访问

  当一个应用运行在64位处理器,计算能力为2.0或者更高的Tesla系列的设备可以访问其它设备的内存。只有cudaDeviceCanAccessPeer()返回true的两个设备上支持这样的特性。

  像下面代码说明一样,必须调用cudaDeviceEnablePeerAccess()函数启用点对点内存访问。

  统一的地址空间被用于两个设备,所以同个指针能引用两个设备上的内存。

cudaSetDevice(0); // Set device 0 as current
float* p0;
size_t size = 1024 * sizeof(float);
cudaMalloc(&p0, size); // Allocate memory on device 0
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
cudaDeviceEnablePeerAccess(0, 0); // Enable peer-to-peer access
// with device 0
// Launch kernel on device 1
// This kernel launch can access memory on device 0 at address p0
MyKernel<<<1000, 128>>>(p0);

3.2.6.5 点对点内存拷贝

  可以在两个设备之间执行内存拷贝。当统一地址空间被用于两个设备,可以定期地调用拷贝函数。

  另外,可以使用cudaMemcpyPeer()、cudaMemcpyPeerAsync()、cudaMemcpy3DPeer()或者cudaMemcpy3DPeerAsync()函数。

cudaSetDevice(0); // Set device 0 as current
float* p0;
size_t size = 1024 * sizeof(float);
cudaMalloc(&p0, size); // Allocate memory on device 0
cudaSetDevice(1); // Set device 1 as current
float* p1;
cudaMalloc(&p1, size); // Allocate memory on device 1
cudaSetDevice(0); // Set device 0 as current
MyKernel<<<1000, 128>>>(p0); // Launch kernel on device 0
cudaSetDevice(1); // Set device 1 as current
cudaMemcpyPeer(p1, 1, p0, 0, size); // Copy p0 to p1
MyKernel<<<1000, 128>>>(p1); // Launch kernel on device 1

  不同设备之间的内存拷贝(在隐式的流0):

  • 先前提交给的两个设备之一的所有命令完成之前,拷贝不会执行
  • 之后的,拷贝先执行完成。

  与正常的流行为一致的,两个设备内存之间异步拷贝可能会与其它流拷贝或者核函数执行同时发生。

  注意,可以通过调用cudaDeviceEnablePeerAccess()函数启用点对点的内存访问,两设备之间的点对点内存拷贝不再需要通过主机,并且更快。

3.2.7 统一虚拟地址空间

  当一个应用运行在64位的处理器上时,只有单一的地址空间被用于主机和计算能力为2.0或者以上的设备。这个地址空间通过调用cudaHostAlloc()函数,用于所有的在主机内存的分配,调用cudaMalloc*()函数分配设备内存。可以调用你cudaPointerGetAttributes()获取一个指针是指向主机还是设备的内存。结论:

  • 当拷贝从或者到一个使用统一地址空间的设备内存时,cudaMemcpy*()函数的cudaMemcpyKind参数是无效的,能被设置为cudaMemcpyDefault
  • 通过cudaHostAlloc()分配的内存自动portable跨那些使用统一地址空间的所有设备,并且cudaHostAlloc()返回的指针能被用于其它设备运行的核函数(不在需要使用cudaHostGetDevicePointer()获得一个设备指针)。

  应用可以检测unifiedAddressing设备属性来查看一个具体的设备是否使用统一地址空间。

3.2.8 进程间通信

  主机线程创建的任何设备内存指针或者事件句柄能直接被任何其它在同一处理器的线程使用。因为在其它的核心无效,所以不能直接得被属于其它处理器的线程所引用。

  为了在不同处理器上共享设备内存指针和事件,应用必需使用进程间通信的API。IPC API只支持Linux上的64位的处理器,并且计算能力在2.0级以上的设备上。

  通过使用cudaIpcGetMemHandle()函数,应用能够得到设备内存指针的IPC句柄,使用标准的IPC机制传递句柄给另外的处理器(进程共享内存或者文件),并且使用cudaIpcOpenMemHandle()从IPC句柄中获得在其它进程合法的指向设备的指针。事件句柄也能使用同样的方式共享。

3.2.9 错误检查

  所有的运行时函数都回返回以恶搞错误代码,但对于异步函数,由于函数在设备完成任务之前返回,所以这个错误代码不能表示异步函数是否有错误发生。这个错误码只能表示执行任务之前在主机发生的错误,典型地比如与无效参数相关;如果异步函数发生错误,将在后来无关的运行时函数调用被表示。

  唯一检查异步错误的方法是在异步调用之后紧接着调用cudaDeviceSynchronize()同步,检查cudaDeviceSynchronize()函数的返回码。

  运行时会给每个主机线程维持一个错误码的变量,并被初始化为cudaSuccess,每次错误发生时都会被重写。cudaPeekAtLastError()返回这个错误码。cudaGetLastError()返回错误码并且设置成为码为cudaSuccess。

  核函数调用不返回任何错误码,cudaPeekAtLastError()和cudaGetLastError()之后返回核函数调用之前发生的错误。

  确保调用cudaPeekAtLastError()或者cudaGetLastError()返回的错误码不是来源于调用核函数先前的,只需要确保在调用核函数之前运行时错误码为cudaSuccess就行,比如在调用核函数之前调用cudaGetLastError()。核函数调用是异步的,所以为了检查异步的错误码,应用必需在核函数调用和cudaPeekAtLastError()或者cudaGetLastError()之间做同步。

  注意cudaStreamQuery()和cudaEventQuery()可能会返回cudaErrorNotReady,cudaErrorNotReady不会被认为是一个错误,因此不会被cudaPeekAtLastError()或者cudaGetLastError()表示。

3.2.10 调用堆栈

  计算能力大于等于2.x的设备,可以通过调用cudaDeviceGetLimit()获得调用栈大小,调用cudaDeviceSetLimit()设置调用栈大小。当调用栈溢出时,通过CUDA调试器(cuda-gdb、Nsight)的应用调用核函数将失败并产生一个栈溢出的错误或者未指定的加载错误或者其它错误。

3.2.11和3.2.12 纹理和曲面内存、图形的互操作性两小节 

  因为与我想要了解的CUDA相关的编程关联不大,因此特地独立到另外的文章中 CUDA C编程入门-编程接口(3.2)CUDA C运行时之纹理和曲面内存、图形的互操作性两小节

你可能感兴趣的:(编程)