极市导读
本文不是一篇教程,而是笔者的一篇总结笔记,概括性地整理CUDA开发相关的经验和知识。 >>加入极市CV技术交流群,走在计算机视觉的最前沿
基本编程模型
开发者可以通过CUDA Runtime API,申请、释放显存,并在内存和显存间进行数据拷贝。 开发者可以编写专用于在GPU上执行的kernel函数,在主机侧通过CUDA C扩展调用kernel函数,调用将创建数以万计的GPU线程,每个GPU线程均会完整执行一次kernel函数,kernel函数内可以对显存进行读、写等各种操作。数以万计的GPU线程之间靠只读的内置变量(线程ID等)互相区分。 一次kernel调用对应的GPU线程,需划分为一个个尺寸相同的线程块。线程块是向GPU进行调度的最小单位,GPU同时支持多个线程块的执行,达到上限后,只有旧的线程块内的线程全部执行完成后,新的线程块才会被调度入GPU。 stream相当于是GPU上的任务队列。每个kernel调用或大多数CUDA API都可以指定关联到某一个stream。同一个stream的任务是严格保证顺序的,上一个命令执行完成才会执行下一个命令。不同stream的命令不保证任何执行顺序。部分优化技巧需要用到多个stream才能实现。如在执行kernel的同时进行数据拷贝,需要一个stream执行kernel,另一个stream进行数据拷贝。
基本硬件架构及其在Kernel执行中的作用
L1 Cache及Shared Memory。 L1 Cache是高速缓存。 Shared Memory是一块可以由开发者编程控制的高速缓存。开发者可以在kernel函数内通过编程手段写入或读取数据。其访问latency远远小于全局显存。 SM分为4个子区域,称为SMSP。每个SMSP包括如下功能单元。 Warp Scheduler+Dispatch Register File寄存器文件 16个INT32、16个FP32、2个Tensor Core及多个LD/ST等单元
Active:被调度到某个SMSP的warp。 Eligible:就绪执行下一条指令的warp(即warp未stalled)。 …
优化技巧
使用异步API
优化内存与显存传输效率
使用Pinned(page-locked) Memory提高传输速度 通过在不同的Stream里同时分别执行kernel调用及数据传输,使数据传输与运算并行。(注意default stream的坑[1]) 尽量将小的数据在GPU端合成大块数据后传输 有些情况下,即使数据不太适合使用kernel处理,但如果为了较低的算法latency,也可权衡传输代价后使用kernel处理数据 注意PCI-e插口的通道个数
优化Kernel访存效率
提高Global Memory访存效率
对Global Memory的访存需要注意合并访存(coalesced )。[2] warp的访存合并后,起始地址及访存大小对齐到32字节 尽量避免跨步访存 8.0及以上的设备可以通过编程控制L2的访存策略提高L2命中率。
提高Shared Memory的访存效率
shared memory由32个bank组成 每个bank每时钟周期的带宽为4字节 连续的4字节单元映射到连续的bank。如0-3字节在bank0,4-7字节在bank1……字节128-131字节在bank0 若warp中不同的线程访问相同的bank,则会发生bank冲突(bank conflict),bank冲突时,warp的一条访存指令会被拆分为n条不冲突的访存请求,降低shared memory的有效带宽。所以需要尽量避免bank冲突。 CUDA 11.0以上可以使用_async-copy_ feature[3]
优化线程级并行
Thread Block 线程块的大小。 如线程块为128,不考虑其它情况下,调度8个线程块到SM即可保持满Occupancy。 如线程块为768,则若调度一个线程块到SM,Occupancy只有0.75,而调度两个线程块则不可能——已超出2080Ti 一个SM最多1024个线程的限制。 每个线程块的Shared Memory使用量 2080Ti每个线程块最多使用48Kb Shared Memory;每个SM只有64Kb Shared Memory。 若线程块尺寸为128,而线程块使用的Shared Memory有30Kb,则由于Shared Memory限制,最多只有两个线程块(256线程)被调度到SM,Occupancy只有0.25。Shared Memory使用60Kb。 若线程块尺寸为128, 每个线程块使用7Kb,则共可调度8个线程块到SM,使用Shared Memory 56Kb。 每个线程使用的Register(寄存器数量) 2080Ti每个SM有65536个32位寄存器,平均到最多1024个线程,则每个线程只能使用64个寄存器。 若某些算法比较复杂需要使用更多寄存器(如矩阵乘法中,需要加载更多数据到寄存器以提高计算访存比),如每线程需要使用128个寄存器,此时由于寄存器限制,SM上最多可以有512线程,此时Occupancy最多为0.5.
指令级优化
提高计算访存比
提高指令级并行
现代不论是CPU还是GPU,指令的执行都是通过流水线进行的,流水线分为多个stage,即一条指令执行完成需要每个stage的工作都执行完成。而一个时钟周期并不是完成一条指令执行的所有时间,而是每一个stage完成当前工作的时间。流水线可以同时执行多条指令的不同阶段。 当后续指令的执行需要依赖前面指令的结果写回寄存器,我们说出现了寄存器依赖。此时后续指令需要等待第前面指令结果写回寄存器才能执行,若后续指令执行时前面指令结果尚未写回寄存器,流水线会失速(stall),此时warp scheduler开始切换到其它eligible warp,若无eligible warp,则SMSP将会空转。 若后续指令不依赖前面指令的结果,则即使前面指令未执行完毕,后续指令也可以开始执行。特别的,即使前序指令是一条耗时几百周期的LDG(全局内存读取)指令或耗时几十周期的LDS(共享内存读取)指令,只要后续一系列指令不依赖读取回来的数据,后续一系列指令可以正常执行而不必等待该LDG/LDS指令执写回寄存器。
数据预取(Prefetch):数据1已读取到寄存器,使用该数据1计算前,先将后续数据2的读取指令发射,再执行一系列数据1的处理指令;这样数据1的处理和数据2的读取在流水线上同时执行着。当数据1处理完成,需要处理数据2时,可以确保数据2已经存在于寄存器中,此时类似的将数据3的读取和数据2的处理同步执行起来。 指令重排:在存在寄存器依赖的指令间插入足够的其它指令,使得后续指令执行时,前面计算指令的结果已写回到寄存器。从CUDA C层面有意识地提供一些语句间的并行性,nvcc编译器可以一定程度上自动进行指令重排。若对nvcc重排结果不满意需要自己重排时,官方尚未开放SASS汇编器,目前只存在一些第三方SASS汇编器工具[5]。 提高Register的效率
Register File也存在bank冲突,但在CUDA C层面上没有直接办法进行物理寄存器控制。 可以通过SASS汇编器,人工进行指令寄存器分配,以尽量消除register bank conflict。 可以通过SASS汇编器,为寄存器访问添加reuse标记,以尽量消除register bank conflict。
使用TensorCore进一步加速矩阵运算[6]
load_matrix_sync
, store_matrix_sync
, mma_sync
等API。使用CUDA生态的各库
cuBLAS[7] TensorRT[8] cudnn[9] NVCodeC[10] DeepStream[11] nvJPEG[12] NCCL[13] CUTLASS [14]
参考
^Default Stream Implicit Synchronization https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#implicit-synchronization
^合并访存官方文档 https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#coalesced-access-to-global-memory
^async-copy feature https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#async-copy
^Occupancy https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html#occupancy
^CuAssembler https://github.com/cloudcores/CuAssembler
^Warp Matrix Functions https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#wmma
^cublas https://developer.nvidia.com/cublas
^tensorrt https://developer.nvidia.com/tensorrt
^cudnn https://developer.nvidia.com/cudnn
^nvidia-video-codec-sdk https://developer.nvidia.com/nvidia-video-codec-sdk
^deepstream-sdk https://developer.nvidia.com/deepstream-sdk
^nvjpeg https://developer.nvidia.com/nvjpeg
^nccl https://developer.nvidia.com/nccl
^cutlass https://github.com/NVIDIA/cutlass
公众号后台回复“数据集”获取100+深度学习各方向资源整理
极市干货
点击阅读原文进入CV社区
收获更多技术干货