CUDA学习笔记 (一)

CUDA学习笔记 (一)

参考教程
1. QINZHAOYU/CudaSteps
2. cuda编程(一)基础
3. CUDA C/C++ 教程一:加速应用程序

一. GPU 硬件与 CUDA 程序开发工具

在由 CPU 和 GPU 构成的异构计算平台中,通常将起控制作用的 CPU 称为 主机(host)
将起加速作用的 GPU 称为 设备(device)
主机和设备都有自己的 DRAM,之间一般由 PCIe 总线连接。

GPU 计算能力不等价于计算性能;表征计算性能的一个重要参数是 浮点数运算峰值(FLOPS)。
浮点数运算峰值有单精度和双精度之分。对于 Tesla 系列的 GPU,双精度下 FLOPS 一般是单精度下的 1/2; 对于 GeForce 系列的 GPU,双精度下 FLOPS 一般是单精度下的 1/32。

CUDA 提供两层 API,即 CUDA 驱动APICUDA 运行时API


二. CUDA中的线程组织

1. nvcc 编译C++代码

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

2. CUDA程序中使用核函数

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

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

    return 0;
}

3. 核函数特点:

1)必须加 global 限定;
2)返回类型必须是空类型 void
3)核函数不支持 c++ 的 iostream
4)核函数调用方式:

hello_from_gpu<<<1, 1>>>   # <<<grid size, block size>>>

主机在调用一个核函数时,必须指明在设备中指派多少线程。核函数中的线程常组织为若干线程块:
A. 三括号中第一个数字是线程块的个数(number of thread block);
B. 三括号中第二个数字是每个线程块中的线程数(number of thread in per block)。
一个核函数的全部线程块构成一个网格(grid),线程块的个数称为网格大小(grid size);每个线程块中含有相同数目的线程,该数目称为线程块大小(block size)。
所以,核函数的总的线程数即 网格大小*线程块大小。

调用核函数后,调用 CUDA 运行时 API 函数,同步主机和设备,作用:等待 Device 代码(GPU) 执行完毕,再在CPU上继续执行。

cudaDeviceSynchronize(); // 与其他并行化的代码类似,核函数启动方式为异步,即 CPU 代码将继续执行而不会等待核函数执行完成;但此行代码:可以让Host 代码(CPU) 等待 Device 代码(GPU) 执行完毕,再在CPU上继续执行。

4. CUDA 的线程组织

4.1 CUDA 的线程组织

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

hello_from_gpu<<<2, 4>>>

网格大小是2,线程块大小是4,总线程数即8。核函数中代码的执行方式是 “单指令-多线程”,即每个线程执行同一串代码。
从开普勒架构开始,最大允许的网格大小是 2^31 - 1(一维网格), 最大允许的线程块大小是 2^10 (1024)。

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

  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),每个线程束由32个连续的线程组成。
对于从开普勒架构到图灵架构的 GPU,网格大小在 x, y, z 方向的最大允许值为 (2^31 - 1, 2^16 - 1, 2^16 -1);线程块大小在 x, y, z 方向的最大允许值为 (1024, 1024, 64),同时要求一个线程块最多有 1024 个线程。

拓展
<<<>>>运算符对kernel函数完整的执行配置参数形式是<<<Dg, Db, Ns, S>>>

1)参数Dg用于定义整个grid的维度和尺寸,即一个grid有多少个block。为dim3类型。Dim3 Dg(Dg.x, Dg.y, 1)表示grid中每行有Dg.x个block,每列有Dg.y个block,第三维恒为1(目前一个核函数只有一个grid)。整个grid中共有Dg.xDg.y个block,其中Dg.x和Dg.y最大值为65535。
2)参数Db用于定义一个block的维度和尺寸,即一个block有多少个thread。为dim3类型。Dim3 Db(Db.x, Db.y, Db.z)表示整个block中每行有Db.x个thread,每列有Db.y个thread,高度为Db.z。Db.x和Db.y最大值为512,Db.z最大值为62。一个block中共有Db.x
Db.y*Db.z个thread。计算能力为1.0,1.1的硬件该乘积的最大值为768,计算能力为1.2,1.3的硬件支持的最大值为1024。
3)参数Ns是一个可选参数,用于设置每个block除了静态分配的shared Memory以外,最多能动态分配的shared memory大小,单位为byte。不需要动态分配时该值为0或省略不写。
4)参数S是一个cudaStream_t类型的可选参数,初始值为零,表示该核函数处在哪个流之中。

4.2 CUDA头文件

CUDA 头文件的后缀一般是 “.cuh”;
同时,可以包含c/cpp 的头文件 “.h”、“.hpp”,采用 nvcc 编译器会自动包含必要的 cuda 头文件,
如 <cuda.h>, <cuda_runtime.h>,同时前者也包含了c++头文件 <stdlib.h>。

4.3 使用 nvcc 编译 CUDA 程序

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

编译流程
nvcc 会先将设备代码编译为 PTX(parrallel 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)

4.4 显卡架构和计算能力

Jetson Orin 模块包含以下内容:
具有多达 2048 个 CUDA 内核和多达 64 个张量内核的 NVIDIA Ampere 架构 GPU

安培Ampere (CUDA 11 ~今) SM80 or SM_80, compute_80 – NVIDIA A100 (不再用特斯拉(Tesla)做名字了 – GA100), NVIDIA DGX-A100
SM86 or SM_86, compute_86 – (from CUDA 11.1 onwards) Tesla GA10x, RTX Ampere – RTX 3080, GA102 – RTX 3090, RTX A6000, RTX A40


三. 简单 CUDA 程序的基本框架

1. 对于单源文件的 cuda 程序,基本框架为:

包含头文件

定义常量或宏

声明 c++ 自定义函数和 cuda 核函数的原型

int main()
{
    1. 分配主机和设备内存
    2. 初始化主机中数据
    3. 将某些数据从主机复制到设备
    4. 调用核函数在设备中计算
    5. 将某些数据从设备复制到主机
    6. 释放主机和设备内存
}

2. CUDA 核函数的要求:

1)返回类型必须是 void,但是函数中可以使用 return(但不可以返回任何值);
2) 必须使用限定符 glolbal,也可以加上 c++ 限定符;
3) 核函数支持 c++ 的重载机制;
4) 核函数不支持可变数量的参数列表,即参数个数必须确定;
5) 一般情况下,传给核函数的数组(指针)必须指向设备内存(“统一内存编程机制”除外);
6) 核函数不可成为一个类的成员(一般以包装函数调用核函数,将包装函数定义为类成员);
7) 在计算能力3.5之前,核函数之间不能相互调用; 之后,通过“动态并行”机制可以调用;
8) 无论从主机调用还是从设备调用,核函数都在设备中执行(“<<<,>>>”指定执行配置)。

3. 自定义设备函数

核函数可以调用不带执行配置的自定义函数,即 设备函数。
设备函数在设备中执行、在设备(gpu)中被调用;而核函数在设备中执行、在主机(cpu)中被调用。
语法规则
1)__global__ 修饰的函数称为核函数,一般由主机调用、在设备中执行;
2)__device__ 修饰的函数称为设备函数,只能被核函数或其他设备函数调用、在设备中执行;
3)__host__ 修饰主机段的普通 c++ 函数,在主机中被调用、在主机中执行,一般可以省略;
4)可以同时用 __host____device__ 修饰函数,从而减少代码冗余,此时编译器将 5)分别在主机和设备上编译该函数;
6)不能同时用 __global____device__ 修饰函数;
7)不能同时用 __global____host__修饰函数;
9)可以通过 __noinline__ 建议编译器不要将一个设备函数当作内联函数;
10)可以通过 __forceinline__ 建议编译器将一个设备函数当作内联函数。
11)设备函数可以有返回值。

未完待续…下篇文章:

猜你喜欢

转载自blog.csdn.net/weixin_36354875/article/details/125683170
今日推荐