CudaSteps/capter2/ReadMe.md

6.3 KiB
Raw Blame History

CUDA 中的线程组织

CUDA 虽然支持 C++ 但支持得并不充分,导致 C++ 代码中有很多 C 代码的风格。

CUDA 采用 nvcc 作为编译器,支持 C++ 代码nvcc 在编译 CUDA 程序时,
会将纯粹的 c++ 代码交给 c++ 编译器,自己负责编译剩下的 cu 代码。


C++ 的 Hello World 程序

>> g++ hello.cpp -o ./bin/hello.exe
>> ./bin/hello
msvc: hello world!

CUDA 的 Hello World 程序

使用 nvcc 编译纯粹 c++ 代码

>> nvcc -o ./bin/hello_cu.exe hello.cu 
>> ./bin/hello_cu.exe
nvcc: hello world!

在该程序中其实并未使用 GPU。

使用 核函数 的 CUDA 程序

一个利用了 GPU 的 CUDA 程序既有主机代码,又有设备代码(在设备中执行的代码)。
主机对设备的调用是通过 核函数kernel function 实现的。

int main()
{
    主机代码
    核函数的调用
    主机代码

    return 0
}

核函数与 c++ 函数的区别:

  1. 必须加 __global__ 限定;

  2. 返回类型必须是空类型 void

    global void hell_from__gpu() { // 核函数不支持 c++ 的 iostream。 printf("gpu: hello world!\n"); }

调用核函数的方式:

hello_from_gpu<<<1, 1>>>

主机在调用一个核函数时,必须指明在设备中指派多少线程。核函数中的线程常组织为若干线程块:

  1. 三括号中第一个数字是线程块的个数number of thread block
  2. 三括号中第二个数字是每个线程块中的线程数number of thread in per block

一个核函数的全部线程块构成一个网格grid线程块的个数称为网格大小grid size
每个线程块中含有相同数目的线程该数目称为线程块大小block size

所以,核函数的总的线程数即 网格大小*线程块大小:

hello_from_gpu<<<grid size, block size>>>

调用核函数后,调用 CUDA 运行时 API 函数,同步主机和设备:

cudaDeviceSynchronize();

核函数中调用输出函数,输出流是先存放在缓冲区的,而缓冲区不会自动刷新。


CUDA 的线程组织

核函数的总线程数必须至少等于计算核心数时才有可能充分利用 GPU 的全部计算资源。

hello_from_gpu<<<2, 4>>>

网格大小是2线程块大小是4总线程数即8。核函数中代码的执行方式是 “单指令-多线程”,
即每个线程执行同一串代码。

从开普勒架构开始,最大允许的线程块大小是 2^10 (1024),最大允许的网格大小是 2^31 - 1一维网格

线程总数可以由两个参数确定:

  1. gridDim.x, 即网格大小;
  2. blockDim.x, 即线程块大小;

每个线程的身份可以由两个参数确定:

  1. blockIdx.x, 即一个线程在一个网格中的线程块索引,[0, gridDm.x);
  2. threadIdx.x, 即一个线程在一个线程块中的线程索引,[0, blockDim.x);

网格和线程块都可以拓展为三维结构(各轴默认为 1

  1. 三维网格 grid_size(gridDim.x, gridDim.y, gridDim.z);
  2. 三维线程块 block_size(blockDim.x, blockDim.y, blockDim.z);

相应的,每个线程的身份参数:

  1. 线程块ID (blockIdx.x, blockIdx.y, blockIdx.z);
  2. 线程ID (threadIdx.x, threadIdx.y, threadIdx.z);

多维网格线程在线程块上的 ID

tid = threadIdx.z * (blockDim.x * blockDim.y)  // 当前线程块上前面的所有线程数
    + threadIdx.y * (blockDim.x)               // 当前线程块上当前面上前面行的所有线程数
    + threadIdx.x                              // 当前线程块上当前面上当前行的线程数

多维网格线程块在网格上的 ID:

bid = blockIdx.z * (gridDim.x * gridDim.y)
    + blockIdx.y * (gridDim.x)
    + blockIdx.x

一个线程块中的线程还可以细分为不同的 线程束thread warp,即同一个线程块中
相邻的 warp_size 个线程(一般为 32

对于从开普勒架构到图灵架构的 GPU网格大小在 x, y, z 方向的最大允许值为 2^31 - 1, 2^16 - 1, 2^16 -1
线程块大小在 x, y, z 方向的最大允许值为 1024 1024 64同时要求一个线程块最多有 1024 个线程。


CUDA 的头文件

CUDA 头文件的后缀依然是 “.h”同时采用 nvcc 编译器会自动包含必要的 cuda 头文件,
如 <cuda.h>, <cuda_runtime.h>同时前者也包含了c++头文件 <stdlib.h>。


使用 nvcc 编译 CUDA 程序

nvcc 会先将全部源代码分离为 主机代码 和 设备代码;主机代码完整的支持 c++ 语法,而设备代码只部分支持。

nvcc 会先将设备代码编译为 PTXparrallel thread execution伪汇编代码再将其编译为二进制 cubin目标代码。
在编译为 PTX 代码时,需要选项 -arch=compute_XY 指定一个虚拟架构的计算能力;在编译为 cubin 代码时,
需要选项 -code=sm_ZW 指定一个真实架构的计算能力,以确定可执行文件能够使用的 GPU。

真实架构的计算能力必须大于等于虚拟架构的计算能力,例如:

-arch=compute_35  -code=sm_60  (right)
-arch=compute_60  -code=sm_35  (wrong)

如果希望编译出来的文件能在更多的GPU上运行则可以同时指定多组计算能力例如

-gencode arch=compute_35, code=sm_35
-gencode arch=compute_50, code=sm_50
-gencode arch=compute_60, code=sm_60

此时编译出来的可执行文件将包含3个二进制版本称为 胖二进制文件fatbinary

同时nvcc 有一种称为 **实时编译just-in-time compilation**机制可以在运行可执行文件时从其中保留的PTX
代码中临时编译出一个 cubin 目标代码。因此, 需要通过选项 -gencode arch=compute_XY, code=compute_XY
指定所保留 PTX 代码的虚拟架构, 例如:

-gencode arch=compute_35, code=sm_35
-gencode arch=compute_50, code=sm_50
-gencode arch=compute_60, code=sm_60  
-gencode arch=compute_70, code=compute_70

于此同时nvcc 编译有一个简化的编译选项 -arch=sim_XY,其等价于:

-gencode arch=compute_XY, code=sm_XY  
-gencode arch=compute_XY, code=compute_XY

关于 nvcc 编译器的更多资料: nvcc