CUDA编程指南阅读笔记_2

4.2 兼容性

      1、二进制兼容性

      二进制代码是设备相关的,使用NVCC编译器编译时,若指定-code选项,则会编译产生目标设备的二进制cubin对象。例如,编译时使用-code=sm_13会产生适用于计算能力1.3的二进制代码。二进制代码在CUDA计算设备上具有小版本的向前兼容性,但是在大版本上不具备兼容性。也就是说,对于计算能力X.y的硬件,使用-code=sm_Xy编译后,程序能够运行于计算能力X.z(其中z>=y)的硬件上,但不能运行在计算能力M.n(M!=X)的硬件上。

      2、PTX代码兼容性

      不同计算能力的设备所支持的PTX指令条数是不同的,一些PTX指令只在拥有较高计算能力的设备上被支持。例如,全局内存(global Memory)的原子操作指令只能用于计算能力不小于1.1的设备;双精度浮点运算指令只能用于计算能力不小于1.3的设备。在将C语言编译为PTX代码时,NVCC使用-arch编译选项指定PTX代码目标设备的计算能力。因此,要想使用双精度运算,编译时必须使用选项-arch=sm_13(或使用更高的计算能力),否则NVCC会自动将双精度操作降级为单精度操作。

      为某一特定设备产生的PTX代码,在运行时总是能够被具有更高计算能力的设备JIT编译为可执行的二进制代码。

      3、应用程序兼容性

      执行CUDA程序有两种方式,一种是直接加载编译好的CUDA二进制代码运行,另一种是首先加载程序中的PTX代码,再执行JIT编译得到二进制的设备可执行文件,然后运行。特别需要注意的是,为了让程序运行具有更高计算能力的未来设备上,必须让程序加载PTX代码。

      事实上,在一个CUDA C程序中可以嵌入不止一个版本的PTX/二进制代码。那么,具体执行时哪一个版本的PTX或者二进制代码会得到执行呢?答案是:最兼容的那个版本。例如编译一个名为x.cu的CUDA源代码:

将会产生兼容计算能力1.1硬件的二进制代码(第一排的-gencode选项)以及兼容计算能力1.1设备的PTX和二进制代码,这些代码都将会嵌入到编译后的目标文件中。

      主机端将会产生一些额外的代码,在程序运行时,这些代码会自动决定装载哪一个版本的代码来执行。对于上面的例子:

  • 计算能力1.0的设备运行该程序将会装载1.0版本的二进制代码
  • 计算能力1.1、1.2或者1.3的设备运行该程序将会装载1.1版本的二进制代码
  • 计算能力2.0或者更高的设备运行该程序将会装载1.1版本的PTX代码进而对其进行JIT编译得到相应设备的二进制代码

      同时,x.cu还可以在程序中使用一些特殊的宏来改变不同设备的代码执行路径。例如,对于计算能力1.1的设备而言,宏__CUDA_ARCH__等于110,在程序中可以对该宏的值进行判断,然后分支执行程序。

      NVCC用户手册列出了很多-arch,-code和-gencode等编译选项的简化书写形式。例如,-arch=sm_13就是-arch=compute_13 -code=compute13, sm_13的简化形式。更多详尽的内容请参阅该手册。

      4、C/C++兼容性

      NVCC编译器前端使用C++语法啊规则来处理CUDA源文件。在主机端,CUDA支持完整的C++语法;而在设备端,只有部分C++语法是被支持的。这方面更为详尽的讨论请参见《CUDA C程序设计指南》的C/C++语言支持章节。

      5、64位兼容性

      64位版本的nvcc编译器将设备代码编译为64位模式,即指针是64位的。运行64位设备代码的先决条件是主机端代码必须也使用64位模式进行编译。同样,32位版本的nvcc将设备代码编译为32位模式,这些代码也必须与相应的32位主机端代码相配合方能运行。

      32位nvcc编译器可以使用-m64编译选项将设备代码编译为64位模式。同时64位nvcc编译器也可使用-m32编译选项将设备代码编译为32位模式。

4.3 CUDA C Runtime

        CUDA C Runtime使用cudart动态链接库实现(cudart.dll或者cudart.so),运行时中所有的入口函数都以cuda为前缀。

4.3.1 初始化

CUDA C Runtime函数库没有明确的初始化函数,在程序第一次调用Runtime库函数时它会自动初始化。因此,在记录Runtime函数调用时间和理解程序中第一个Runtime调用返回的错误代码时,需要将初始化考虑在内。

        在初始化期间,Runtime将会为系统中每一个设备创建一个CUDA上下文(类似CPU中进程的数据结构),这个上下文是设备的基本上下文,它被程序中所有的主机线程所共享。创建过程在后台运行,并且,Runtime将隐藏基本上下文使之对Runtime API这一层次的程序员不可见。

        当一个主机线程调用cudaDeviceReset()函数时,它将会销毁线程当前控制设备的基本上下文。也就是说,当线程下一次调用runtime函数时将会重启初始化,一个新的CUDA基本上下文将被创建出来。

4.3.2 设备内存

        正如前面异构计算章节所讲,CUDA编程模型假定系统是由主机和设备构成的,它们分别具有自己独立的内存空间。Runtime负责设备内存的分配,回收,拷贝以及在主机和设备间传输数据的工作。

        设备内存可以有两种分配方式:线性内存或者CUDA数组

        CUDA数组是一块不透明的内存空间,它主要被优化用于纹理存取。

        线性内存空间与平时我们访问的内存类似,对于计算能力1.x的设备来说,它存在于一个32位的地址空间。对于更高计算能力的设备而言,它存在于一个40位的地址空间中。因此,单独分配的实体可以使用指针来相互应用。

 我们通常使用cudaMalloc()函数分配线性内存空间,使用cudaFree()函数释放线性内存空间,使用cudaMemcpy()函数在主机和设备之间传输数据。下面是CUDA Vector Add代码示例的一些片段:

// 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 = ...;
    size_t size = N * sizeof(float);
 
    // Allocate input vectors h_A and h_B in host memory
    float *h_A = (float*)malloc(size);
    float *h_B = (float*)malloc(size);
 
    // Initialize input vectors
    ...
 
    // Allocate vectors in device memory
    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, size);
    cudaMalloc(&d_B, size);
    cudaMalloc(&d_C, size);
 
    // Copy vectors from host memory to device memory
    cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);
 
    // Invoke kernel
    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
 
    // Copy result from device memory to host Memory
    cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);
 
    // Free device memory
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);
 
    // Free host memory
    ...
}

片段展示了设备内存的分配,传输以及回收过程。

       除了上面展示的方法,我们还可以使用cudaMallocPitch()和cudaMalloc3D()函数来分配线性内存。这些函数能够确保分配的内存满足设备内存访问的对齐要求,对于行地址的访问以及多维数组间的数据传输提供高性能保证,因此非常适合对于二维和三维数组内存空间的分配。

下面的代码片段展示了分配和使用尺寸为width x height的二维数组的技术:

// 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 x height x depth的三维数组的分配和使用方法:

// 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];
        }
    }
}

更多详细的内容请查阅参考手册。

下面的代码示例展示了多种使用Runtime 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()函数。
 

http://blog.csdn.net/csgxy123/article/details/9991047

http://blog.csdn.net/csgxy123/article/details/9995487

猜你喜欢

转载自blog.csdn.net/wss794/article/details/81167132