CUDA Cookbook by Eric

Learning materials

周彬:
官方文档就是最好的教程 docs.nvidia.com/cuda/ 文档很多,推荐前期重点读以下三份文档
CUDA C++ Programming Guide:介绍了CUDA C的编程模型,附录的内容也很丰富
CUDA C++ Best Practices Guide:介绍了怎样写CUDA会有更高的性能
Kernel Profiling Guide:介绍了如何profiling kernel,并透露了更多硬件实现上及kernel执行过程的细节

1. Philosophy

在进行CUDA编程之前,我们首先来看看什么样的程序是是何使用GPU进行加速的,

深蓝学院的老师是这样说的:

  1. 内存读写少
  2. 控制简单
  3. 计算简单
  4. 可并行度高

2. Basics

2.1 Official materials

CUDA Samples

GitHub repo: cuda-samples

2.2 nvidia-smi驱动显示信息

在这里插入图片描述

KeyValueDescription
CUDA Version11.8即 DRIVER API COMPATIBILITY VERSION,表示当前驱动适配的最高CUDA版本,这里显示是11.8, 表示CUDA-11.8 以下的程序都是可以正常运行的。

2.3 GPUs specs

GeForce RTX 2080 TiGeForce RTX 3070 Ti Laptop GPU
GPU Engine Specs:
Processing Blocks (partitions)44
NVIDIA CUDA® Cores43525888
CUDA Cores / MP64
Maximum number of threads per block1024
Technology Support:
NVIDIA Architecture NameTuringAmpere

Note:
CUDA没有提供查询显卡中 Tensor cores 数量的API,所以暂时无法通过API的方式确定 Tensor cores 的数量。

2.4 CUDA API hierarchy

在这里插入图片描述

2.5 CUDA parallel model

AbbreviationFull name
SIMTSingle Instruction, Multiple Thread
SPMDSingle Program, Multiple Data
SPStreaming Processor
SMStreaming Multiprocessors
MP(=SM)Multiprocessors

2.5.1 Thread concepts correspondence

ProgamPhysical
threadSP / core
(invisible)warp (32 cores)
blockSM / multiprocessor

2.5.2 Memory concepts correspondence

ProgamNameAffiliationMax range
__share__share memory (block)blocksharedMemPerBlock
(2080Ti: 48KB)
SM share memoryTotal shared memory per multiprocessorSMsharedMemPerMultiprocessor
(2080Ti: 64KB)

Note:
在CUDA编程模型中,share memory是从属于block的概念;具体而言,sharedMemPerBlock表示在一个核函数中可以申请的共享内存的最大值,对于以下代码来说:

// 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;
    ...
        __shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
        __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];
    ...}

则是要求AsBs总共申请的内存大小不能超过sharedMemPerBlock返回的数值。

2.5.3 关于block池(block pool)的理解

这里援引杨老师关于 block pool 的解释(比较通俗易懂):

【关于block在CUDA中是如何被调度的】
杨老师:从硬件的角度给你们解释吧。
一个GPU是由多个SM组成的,SM之间共享L2 cache和显存,SM内私有 L1 cache(整个SM一个cache)和 shared memory。SM内有计算单元,控制单元,访存单元等等。
GPU内最小的执行单元是warp,最小的调度单元是block。一个block只能在一个SM上执行,一个SM上可以执行多个block。
我们以计算单元为假设,假设一个SM内有1024个计算单元,即同时最大可以执行1024个线程。
你写了一个kernel,有100个block,一个block里有256个线程。这么多block同时执行不了,所以就会有一个block池,等哪个SM的计算单元空闲了,调度单元就从池子里选一个ready状态的block发射给SM执行。

2.5.4 Block和warp的分配和调度由CUDA负责

关于“block到SM的分配”warp的调度,杨老师在群里面说到:

杨老师:
block scheduler都不是透明的,warp scheduler就更别想了。

这说明“block到SM的分发”warp的调度对开发者来说都是不透明的

2.5.5 关于限制block并行数量的原因

  • Shared memory limited case
  • Register limited case

形象的示例如图所示:
在这里插入图片描述

Shared memory limited case

描述:在一个block中使用的共享内存的大小较大,是目前性能优化的瓶颈;
示例分析:例如,现在我们使用的显卡为2080Ti,使用deviceQuery.cu获得的硬件支持信息如下:

Maximum number of threads per block: 1024
Total amount of shared memory per block: 49152 bytes
Total shared memory per multiprocessor: 65536 bytes (64KB)

设核函数kernel的代码为:

kernel<<<grid, 128>>> () {
  …
  # Shared memory申请大小为21KB
  …
};

于是,我们分别从threads和 shared memory 的角度看看,单个SM中最多可以并行的block数量:

limit_threads: 1024/128 = 8
limit_shared_memory: 64//21 = 3

可以看到 limit_shared_memory的值为3,小于limit_threads,所以当前核函数中 shared memory 是主要的roofline。

2.5.6 SM同时最多计算64个数据点(2 warps)

因为每个SM包含64个 CUDA Cores。

2.6 GPU Memory

2.6.1 Shared memory

同一个线程块(Block)中的线程可以通过共享内存进行通信,这是线程协作中一种比较快的方式

__syncthreads():线程同步

文档:CUDA Programming Guide :: __syncthreads()
在读写状态转换的时候,要进行线程同步;

2.6.2 Relationship

L1 cache & Shared memory

关于这两者的关系,深蓝学院的老师是这样说的:

LitLeo:
大部分显卡 L1 cache 和 shared memory 都是使用同一块存储(物理存储器),并且可以通过函数配置各自的大小;在默认情况下,两者是预设的固定大小,(并不会动态变化)。

表格展示了一般情况和使用 Shared memory 情况下的数据传输路径:

TransferencePath
defaultGlobal memory ⇒ L2 cache ⇒ Register
__shared__Global memory ⇒ Shared memory ⇒ Register

2.6.3 Array storage is column-major

在CUDA中,二维数组是以行优先形式存储的;

Note
关于数组(C-array)在CUDA的存储方式,我们在 CUDA-doc 中并没有找到明确的说明;对此,我们在 NVIDIA Forums 上进行了提问,CUDA工程师回复了我们,

Robert_Crovella:
There is no 2D-array or multidimensional array in CUDA C++ that is any different than C++. The storage behavior of C-style arrays in CUDA follows the C++ specification. And, for what it is worth, that is not a column-major storage format/specification.

CUDA中数组的内存排列方式和C++(“C++ specification”,C++规范)相同。

CUDA编程中为什么一般会把高维张量转换为一维数组再进行计算呢?

这是因为一维数组的访存效率要高于多维数组
这里我们引用一些开发者的解释:

未雨:
一维数组相较多维数组的好处:

  • 存储开销少,一维数组只需要一个指针,多维数组除了最后一维外都需要存指针,需要 D 1 ∗ D 2 ∗ . . . ∗ D N − 1 D_1 * D_2 * ... * D_{N-1} D1D2...DN1个指针
  • 访问效率高,一维数组算出index后只需要一次访存就能拿到对应元素,多维数组需要N次

2.7 Error checking

AB
cudaError_t

关于在CUDA编程中进行 error checking 的意义,请参考《tensorRT从零起步高性能部署:初始化和检查的理解,CUDA错误检查习惯》
编写方式:

  1. 使用inline说明符;

3. Device information

AB
cudaGetDeviceCount

Get device properties

cudaDeviceProp prop;
checkRuntime(cudaGetDeviceProperties(&prop,0));
printf("prop.sharedMemPerBlock = %.2f KB\n", prop.sharedMemPerBlock/1024.0f);

4. C++ environment

NVCC支持的C++标准:Select a particular C++ dialect | NVCC :: CUDA Toolkit Documentation
CPP_STANDARD ≤ C++17
Gcc版本要求:1.1. System Requirements | Native Linux Distribution Support in CUDA 11.7

4.2 Windows配置

MSVC编译器路径示例:

"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.34.31933\bin\Hostx64\x64\cl.exe"

5. File

5.1 Format

头文件:.cuh
CUDA-CPP文件:.cu

5.2 Header

CUDA header:

#include <cuda_runtime.h>

Cublus header:

#include <cublas_v2.h>

Note:
对于为什么#include <cublas_v2.h>有一个v2,这是因为v2表示新版的API;而不加v2,则使用的是旧版的API;具体的说明,可以查看cublas的关于API引入的说明文档

6. Context (CUDA Driver API)

cuContext使用起来有一点像一个任务队列;

7. Stream:异步的任务队列

CUDA-stream有点类似于CPU中的thread,可以并行执行多个串行任务;

8. Data copying

CudaMemcpyAsync():异步数据拷贝

文档:CUDA Runtime API :: cudaMemcpyAsync()
cudaMemcpyAsync()在使用时,需要传入 cuda_stream对象;

9. Function

SM包含的最大warp数量:32
Block中包含thread的最大数量:1024 ( = 32 × 32 ) (= 32\times 32) (=32×32)

9.1 __global__: kernel function

文档:CUDA Programming Guide :: __global__ function
__global__修饰的函数是核函数,这些函数在GPU上执行 ,但是需要在CPU端调用;

《CUDA并行程序设计 - GPU编程指南》:
所谓的内核函数就是一个只能在GPU上执行而不能直接在CPU上执行的函数。

并行块大小Db不能超过maxThreadsPerBlock
返回值:必须为void。
异步性:核函数是异步的,会立即返回。

__global__函数支持CPP-style的多维数组索引

经过查看 CUDA Forum 中的帖子,我们知道CUDA是不支持CPP-style的多维数组,也就是说,无法在__global__函数中以“d_array[i][j]”的形式索引二维数组;
示例代码:CUDA_2d_array

GridID & blockID 的计算

int blockId =	blockIdx.z * gridDim.x * gridDim.y + 
				blockIdx.y * gridDim.x + 
				blockIdx.x;
int threadId =	blockId * (blockDim.z * blockDim.y * blockDim.x) +
				(threadIdx.z * (blockDim.y * blockDim.x)) +
				(threadIdx.y * blockDim.x) +
				threadIdx.x;

【南溪笔记】关于block与SM关系的理解

一个block会被分配到一个SM上,而一个SM可能会有多个block同时存在;
“block分配给SM”这个过程是由CUDA自身进行协调的,而不是由编程人员自己定义的,也就是说对编程人员是不透明的。

std::cout不能在__global__函数中使用

std::cout不能在__global__函数中使用,会报错:
在这里插入图片描述
需要使用printf()

10. Array

CUDA核函数中是可以使用二维数组的,请参考“CUDA 2D-array by an array of pointers to 1D arrays”

11. CUDA event

AB
cudaStream_t
cudaEventRecord()

cudaEventRecord

文档:CUDA Programming Guide :: cudaEventRecord()

12. Matrix-matrix operations

12.1 CuBLAS:NVIDIA官方提供的线性代数计算库

CuBLAS API LevelOperations
Level-1perform scalar and vector-based operations
Level-2perform matrix-vector operations
Level-3perform matrix-matrix operations

11.1.1 Basics

函数命名解析:以cublasSgemm为例

cublasSgemm ⇒cublas·S·gemm
cublas:函数库名称;
S:单精度浮点数(Single Precision Floating Point);
gemm:表示 general Matrix Multiply ,即一般矩阵乘法。

常见缩写全称含义
gemvgeneral matrix-vector multiply一般矩阵&向量乘法
gemmgeneral matrix multiply一般矩阵乘法
transa & transb:矩阵变换参数

transa | transb是cublas库中指定矩阵变换的参数,它可以设置三种不同的值:

  • 如果transa设置为CUBLAS_OP_N,表示对矩阵A进行原样计算,即不做任何变换。
  • 如果transa设置为CUBLAS_OP_T,表示对矩阵A进行转置,即将矩阵A中的行和列进行交换。
  • 如果transa设置为CUBLAS_OP_C,表示对矩阵A进行共轭转置,即将矩阵A中的实部和虚部进行交换。

这个参数主要用于调整矩阵A的结构,以满足矩阵乘法运算的要求。

cublasSetStream():cublas默认使用同步流

Cublas在默认情况下使用同步流,为了实现GPU的异步运算,需要显式地设置cublas的任务流;

11.1.2 Matrix-to-matrix Multiply:矩阵乘法

12. CMake configuration for CUDA

CMakeLists.txt模板下载:

wget https://raw.githubusercontent.com/songyuc/Cuda_notes/master/CMakeLists.txt

CMakeLists.txt模板代码:

cmake_minimum_required(VERSION 3.23)

set(CMAKE_CUDA_COMPILER "/usr/local/cuda/bin/nvcc")
# NECESSARY: because we need to specify the CUDA_COMPILER explicitly.
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
    set(CMAKE_CUDA_ARCHITECTURES "75;80;86")
endif()
# Note: This variable is used to initialize the CUDA_ARCHITECTURES property on all targets.

project(Cuda_notes LANGUAGES CUDA CXX C)

set(CMAKE_CXX_STANDARD 23)

set(CMAKE_CUDA_STANDARD 14)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
include_directories(SYSTEM "${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}")
include_directories("${CMAKE_SOURCE_DIR}/cuda-samples/Common")
include_directories(.)

add_executable(Cuda_notes deviceQuery.cu)
target_link_libraries(Cuda_notes cudart)

包含CUDA代码的文件一定要以.cu结尾才能触发nvcc的编译;

5.1 CMAKE_CUDA_ARCHITECTURES

CMAKE_CUDA_ARCHITECTURES用来指定目标显卡的架构算力,内部是用来设置CUDA_ARCHITECTURES变量的值,关于CUDA_ARCHITECTURES变量的具体含义,请参考文档 CUDA_ARCHITECTURES — CMake Doc

13. CUDA debugging: NVIDIA Nsight

13.1 VSCode: NVIDIA Nsight Visual Studio Code Edition

视频教程:CUDA Support in Visual Studio Code with Julia Reid

14. Profiling

AbbreviationFull Name
NVVPNVIDIA Visual Profiler

14.1 C++代码测试CUDA代码的运行时间

文档:CUDA Programming Guide :: Using CUDA GPU Timers
官方模板:

cudaEvent_t start, stop;
float time;

cudaEventCreate(&start);
cudaEventCreate(&stop);

cudaEventRecord( start, 0 );
kernel<<<grid,threads>>> ( d_odata, d_idata, size_x, size_y, 
                           NUM_REPS);
cudaEventRecord( stop, 0 );
cudaEventSynchronize( stop );

cudaEventElapsedTime( &time, start, stop );
cudaEventDestroy( start );
cudaEventDestroy( stop );

使用lambda表达式封装核函数调用

auto launch_kernel = [=]() { // 使用值捕获传递显存数组
    traverseArray<<<gridSize, blockSize>>>(d_array, width, height);
};

14.2 Nsight: NVIDIA Nsight Systems

Nsight是NVVP和nvprof的继任者,(NVVP和nvprof在之后的CUDA版本中会被淘汰掉);

Legacy: NVVP

关于NVVP的使用教程,请参考《深蓝学院 - 深度神经网络加速:cuDNN 与 TensorRT - 第3章:CUDA Stream 和 Event - 第6节:NVVP工具演示》

15. CUDA optimization

15.1 CUDA编程优化目标

  1. 性能优化
  2. 读取优化:数据的重复读取越少越好
  3. 显存优化:显存占用越少越好

15.2 起始的问题:判断当前的算子是存储密集型还是计算密集型?

引用 Fedor Pikus 在《CppCon 2018: Fedor Pikus “Design for Performance”》上讲到的问题:

Fedor Pikus: Is it memory-bound or compute-bound?

15.3 数组下标优化:int64 -> int32

请参考博文《3. 尽量使用int32表示下标 | 开源100天,OneFlow送上“百天大礼包”:深度学习框架如何进行性能优化?》

15.4 条件判断优化:[[likely]] / [[unlikely]] [Doc]

15.5 Kernel合并

理论依据:

Kernel合并可以减少访存的次数,如图所示,
在这里插入图片描述

15.6 多Stream并行优化

多Stream并行优化的案例也是存在的,OneFlow中就使用过多个Stream来优化MLP的梯度计算:
oneflow/user/kernels/cublas_fused_mlp_grad_kernel.cu

15.7 NVCC也有优化标志

常用的优化标志包括-O || -O2 || -O3

15. Precision alignment

对于float,精度对齐在1e-5左右;

16. 常见编程范式

16.1 使用宏定义函数进行 cudaError_t 检查

示例代码(rbgirshick/py-faster-rcnn):

#define CUDA_CHECK(condition) \
  /* Code block avoids redefinition of cudaError_t error */ \
  do { \
    cudaError_t error = condition; \
    if (error != cudaSuccess) { \
      std::cout << cudaGetErrorString(error) << std::endl; \
    } \
  } while (0)

17. 图像处理:npp

NPP: Nvidia 2D image and signal Performance Primitives
NVIDIA NPP is a library of functions for performing CUDA accelerated 2D image and signal processing.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
The CUDA Handbook begins where CUDA by Example (Addison-Wesley, 2011) leaves off, discussing CUDA hardware and software in greater detail and covering both CUDA 5.0 and Kepler. Every CUDA developer, from the casual to the most sophisticated, will find something here of interest and immediate usefulness. Newer CUDA developers will see how the hardware processes commands and how the driver checks progress; more experienced CUDA developers will appreciate the expert coverage of topics such as the driver API and context migration, as well as the guidance on how best to structure CPU/GPU data interchange and synchronization. The accompanying open source code-more than 25,000 lines of it, freely available at www.cudahandbook.com-is specifically intended to be reused and repurposed by developers. Designed to be both a comprehensive reference and a practical cookbook, the text is divided into the following three parts: Part I, Overview, gives high-level descriptions of the hardware and software that make CUDA possible. Part II, Details, provides thorough descriptions of every aspect of CUDA, including * Memory * Streams and events * Models of execution, including the dynamic parallelism feature, new with CUDA 5.0 and SM 3.5 * The streaming multiprocessors, including descriptions of all features through SM 3.5 * Programming multiple GPUs * Texturing The source code accompanying Part II is presented as reusable microbenchmarks and microdemos, designed to expose specific hardware characteristics or highlight specific use cases. Part III, Select Applications, details specific families of CUDA applications and key parallel algorithms, including * Streaming workloads * Reduction * Parallel prefix sum (Scan) * N-body * Image ProcessingThese algorithms cover the full range of potential CUDA applications.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值