GPU史前文明
HPC高性能计算:是一个很早就有的领域. GPU和CPU的结合在HPC领域成为趋势。
冯诺依曼架构处理器
当代几乎所有的处理器都是冯诺依架构: 处理器持续从存储器取指令、指令译码、执行指令的指令周期。 CPU由控制器、存储器、运算器组成。
内存速度和时钟速度的比率是限制处理器吞吐量的重要因素。大多数程序都是内存受限型而不是时钟受限型,CPU引入三级缓存来加速。 缓存越多,芯片成本越高,成品难度也越大。
克雷向量处理器
70年代,克雷计算机,其设计结构对后续GPU有很深的影响
- 多个处理器共享内存
- 一个操作处理多个操作数
连接机并行指令集
80年代,连接机,其SIMD处理方式与后续处理器SIMD基本一致
- 早期的并行指令集,都是为解决图像处理、视频处理而设计出现的。
- 采用的是标准的单指令多数据 (SIMD) 并行处理。连接机通过这种设计消除多余的访存操作,并将内存读写周期变为原来的 1/16
- 64k个处理器,每个核都在其数据集上执行SIMD指令
SIMD: 一条指令处理多条数据,用于小碎数据的并行操作。比如i7是64位处理器,一次最多处理64位(8个字节)数据。早先处理这些数据只能用于他们的低8位,如果把64位寄存器拆成8个8位寄存器就能同时完成8个操作,计算效率提升了8倍.
- MMX: MMX将64位寄存当作2X32或8X8来用,只能处理整形计算.
- SSE: 浮点数支持,1/2/3/4, 指令数越来越多, Intel先出,然后AMD跟随
- AVX: AMD抢先除了SSE5,随即表示,不会支持SSE5,转而发布AVX. 由于SSE5和AVX指令集功能类似,并且AVX包含更多的优秀特性,因此AMD决定支持AVX指令集,避免让软件开发者因为要面对两套不同指令集而徒增开发难度。
CELL处理器:
20世纪初,IBM、索尼合作研发,它和之后Nvidia的GPU很类似。
- 一个 PPC 处理器作为监管处理器,与大量的SPE流处理器相连通,组成了一个工作流水线。
- 每个SPE核可执行独立的程序,不同SPE核的处理可以不同,比如一个SPE做
- PPC核取一组待处理数据,分配给若干个SPE核,执行完毕后PPC核再取回。
- 环形流水线,每个SPE核执行一种操作
多点计算:
Bonic、Folding@home、Bitcoin、Hadoop等点计算项目出现。
单节点算力受主频太高,电力和散热成本上升,收益递减。使用多个单核实现集群并行处理(网络互联、PCIE互联),成本更低。
GPGPU编程
GPU的应用早已不限于图像领域。 使用GPU做通用目的的并行计算(而不是仅仅用于图形渲染)。 这已经跟CUDA的设计思想一致了 http://graphics.stanford.edu/courses/cs148-10-fall/lectures/programmable.pdf
- 整型并行处理
- 浮点数并行处理
- 向量并行处理
典型操作
- map
- reduce
- stream filtering
- scan
- scatter
- gather
- sort
- search
GPU产品线
GPU 首字母 Graphics, 是为了图形处理而设计的处理器。
GPU原先作为显卡的芯片处理器,随着并行计算的发展,发展到计算卡的领域。 GPU和显卡(图形卡) -> GPU和计算卡
显卡都有DVI接口、VGA接口、HDMI接口,而计算卡/加速卡只有PCIE接口。
- 显卡市场: (2021Q2)Intel以68.3%位居第一位,AMD和NVIDIA分列二三位。
- 独立显卡市场: (2021Q2) Nvida占83%, AMD占17%。
- 计算卡市场: 主要是云端AI芯片市场,GPU(50%)、FPGA(30%)、ASIC(20%)
Nvida GPU
- Tesla产品: 应用于深度学习、高性能计算
- Quadro、RTX产品: 应用于专业可视化设计
- NVS产品: 应用于商业级图像显卡
- GeForce、TITAN 产品: 应用于消费级娱乐显卡
AMD GPU
- AMD Instinct: 应用于计算卡
- AMD Radeon: 应用于显示卡
Intel GPU
- Xe架构
ARM GPU
- Mali GPU: 基于tile-based的渲染原理,旨在通过最小化帧缓冲访问所需的外部DDR存储器带宽来提高系统范围的能效
Apple GPU
- M1新品: 内置了8核GPU,深度学习引擎
并行设计模式
多核并发OpenMP标准
单个节点内部实现并行处理,多核处理器共享存储并行(shared-memory parallel programming)。
拆成了几个线程,然后使用事件对象等待所有线程结束。
类似技术 GCD、TBB
多机并发MPI标准
多个节点间的并行处理,计算机集群共享通信并行。
其主要瓶颈在网络通信上(Ethernet、infiniband)。
并行问题
并发时资源共享,需要引入semaphore、mutex等机制。
易并行性
有一些问题属于 “易并行” 问题:如矩阵乘法。在这类型问题中,各个运算单元输出的结果是相互独立的,这类问题能够得到很轻松的解决 (通常甚至调用几个类库就能搞定问题)。 然而,若各个运算单元之间有依赖关系,那问题就复杂了。在 CUDA 中,块内的通信通过共享内存来实现,而块间的通信,则只能通过全局内存。
局部性
操作系统原理里讲过局部性原理,简单来说就是将之前访问过的数据 (时间局部性) 和之前访问过的数据的附近数据 (空间局部性) 保存在缓存中。 计算机局部性原理指导处理器逐渐设计出了多级缓存、超线程,提供ALU的利用率。
- CPU设计上对软件程序员屏蔽这些事情。
- GPU设计上,让软件程序员主动处理局部性问题,提前把数据加载到片内存储,程序员来控制写回缓存”脏数据”操作的时间。从而提升性能。
基于任务的并行
这种并行模式将计算任务拆分成若干个小的但不同的任务,如有的运算单元负责取数,有的运算单元负责计算,有的负责…… 这样一个大的任务可以组成一道流水线。其瓶颈在于效率最低的运算单元。
基于数据的并行
这种并行模式将数据分解为多个部分,让多个运算单元分别去计算这些小块的数据,最后再将其汇总起来。
可并行化的模式
- for-loop并行: 将循环拆分为并行, 要注意依赖性。
- fork-join并行: 动态并行性,并发事件数目之前不确定。
- tiling-grid: 有些大的数据可以分条分块并行处理。
- Divide-conquer : 递归recursive -> 迭代iterative
(X)PU计算单元
- SISD: 单指令单数据流. 串行程序设计
- SIMD: 单指令多数据流. 数据并行设计,同一时刻只运行一个指令流。单核可以做的更小,能耗更低。
- MISD: 多指令单数据流. 把多条指令组合成一条指令,就能达到其效果。没有纯粹的MISD处理器。
- MIMD: 多指令多数据流. PC多核CPU,线程工作池,OS负责分配线程到N个CPU核上执行,每个线程具有一条独立的指令流,整个CPU对多个核的不同指令流同时解码执行(多条解码通路)。
数据并行的简单描述:
对一个数据一个进行操作 -> 对一组数据进行一个操作。
- 对这组数据中每个元素所需的操作都是相同的,所以(X)PU访问程序存储区取指令然后译码这件事只需做一次。
- 数据区间有界且连续,数据从内存读取也是一次全部取出。
SIMT
Nvidia GPU版的SIMD又成为SIMT,单指令多线程,其指令操作码跟CPU的不同,需要程序通过一个内核程序指定每个线程的工作内容。
SIMT 每一个core有自己的寄存器、自己的ALU、自己的data cache, 没有独立的instruction cache、没有独立的解码器、没有独立的程序计数器。
SIMD是一个单独的线程,只是这个线程能同时进行多个计算而已. 比如SIMD单元128-bit宽,包含16个128-bit的寄存器,能够被用来当做32个64-bit寄存器。这些寄存器能被当做是同等数据类型的的vector。 SIMT多个线程各有各的处理单元,和SIMD公用一个ALU不同。因而可以进行更为复杂的并行计算。
面向GPU编程思想上的差异
程序中能够并行运行的代码占多大比例?
单线程CPU程序员 vs GPU上的并行程序员
- 大多数的程序还都是串行主导。(分时复用达到并发效果,不算是真正的并行)
- 并行处理带来复杂度的提高,设计GPU软件时程序员需要把并发性和局部性作为关键问题来考虑。
CUDA
2007年,nvidia发现了一个能使得GPU进入主流的契机,那就是为GPU制定一个标准的编程接口,这就是CUDA.
CUDA编译模型使用了和java语言一样的编译原则:基于虚拟指令集的运行时编译。
它使 NVIDIA GPU 能够执行使用 C、C++、Fortran、OpenCL、DirectCompute 和其他语言编写的程序.
CUDA的替代选择(通用并行计算平台和编程模型)
- OpenCL 苹果
- DirectCompute 微软DirectX的一部分。
- ROCm AMD
标准
- OpenACC: 让并行程序员能够为编译器提供简单的提示,亦称「指令」,使编译器能够识别哪些代码部分需要加速,无需程序员修改或改编底层代码本身。
概念
- context: 在使用GPU进行计算时,需要创建一个上下文来管理GPU资源的分配和释放。
- stream: 流是GPU上执行的一系列操作的序列。GPU操作可以被分成多个流,每个流中的操作可以并行执行。通过将操作分配到不同的流中,可以提高GPU的利用率和性能。
- kernel: 核函数是在GPU上执行的并行计算任务。它是GPU程序的基本执行单元,通常由多个线程同时执行。核函数可以通过编程语言(如CUDA)编写,并在GPU上执行,以实现高性能的并行计算。
- graph: CUDA图(CUDA Graph)是CUDA中的一项功能,提供了一组API用于创建、管理和执行CUDA图。CUDA图的主要目的是将多个GPU计算任务的依赖关系显式地表示出来,并通过这些依赖关系来优化GPU计算的执行顺序。
- CUDA Graphs 在 CUDA 10 中首次亮相,它让一系列 CUDA 内核被定义和封装为一个单元,即一个算子图,而不是一系列单独启动的算子。它提供了一种通过单个 CPU 操作 launch 多个 GPU 算子的机制,从而减少了 launch 开销。
import numpy as np
from numba import cuda
# 创建GPU上下文 ,它用于管理GPU资源和执行GPU操作。
cuda_context = cuda.create_context()
# 创建GPU流 它用于将操作分配到不同的流中以实现并行执行。
stream = cuda.stream()
# 定义一个核函数,用于在GPU上执行并行计算
@cuda.jit
def vector_add(a, b, c):
idx = cuda.threadIdx.x + cuda.blockIdx.x * cuda.blockDim.x
if idx < len(c):
c[idx] = a[idx] + b[idx]
# 创建输入数据和输出数据
a = np.array([1, 2, 3, 4, 5])
b = np.array([6, 7, 8, 9, 10])
c = np.zeros_like(a)
# 在GPU上分配内存
d_a = cuda.to_device(a, stream)
d_b = cuda.to_device(b, stream)
d_c = cuda.to_device(c, stream)
# 在GPU上执行核函数
threads_per_block = 32
blocks_per_grid = (len(c) + (threads_per_block - 1)) // threads_per_block
vector_add[blocks_per_grid, threads_per_block, stream](d_a, d_b, d_c)
# 将结果从GPU复制回CPU
d_c.copy_to_host(c, stream)
# 同步GPU流
stream.synchronize()
# 打印结果
print(c)
# 销毁GPU上下文
cuda_context.pop()
CUDA 线程层次
一个CUDA Kernel大概可以分为这么几层(从底层到顶层):thread < warp < block < grid PS:core和kernel两个词,core指的是硬件核心,处理器;kernel指软件核心,可重复使用的种子。
- Grid 网格: 一维或多维线程块(block)
- Block 线程块: 一组线程(thread),包含若干个warp。
- Warp 线程束: 以批处理方式运行的多个线程(32个), 一个SIMD处理中若干个线程是同时执行的。
- Thread 线程: 一个CUDA的并行程序会有许多threads来执行, 线程是GPU运算的最小执行单元。
- Kernel definition: 使用__global__定义的C/C++函数。
在CUDA编程模型中,一个”lane”通常指的是一个在GPU上执行的线程在其所属的warp(线程束)中的唯一索引。CUDA中的一个warp是一组同时执行的线程,NVIDIA的GPU中这个数目通常是32个线程。
shared memory
共享内存被分成了若干个等宽的存储段,这些存储段被称为“banks”,硬件上的限制是,每个bank在一个时钟周期内只能服务一个访问请求。
一个block要访问shared memory,只要能够保证以其中相邻的16个线程一组访问thread,每个线程与bank是一一对应就不会产生bank conflict。否则会产生bank conflict,访存时间成倍增加,增加的倍数由一个bank最多被多少个thread同时访问决定。有一种极端情况,就是所有的16个thread同时访问同一bank时反而只需要一个访问周期,此时产生了一次广播。
CUDA GPU 物理层次
一张GPU卡由若干个流处理簇(SM)组成,一个SM配置若干个流处理器(SP),
- 内存(全局的、常量的、共享的)
- 流处理器簇(SM)
- 每个SM内有多个SP共享的、程序可控的高速缓存
- 每个SM内有一个寄存器文件
- 流处理器/流处理单元(SP)
- 一个SM里的所有SP共享内存和指令单元。
- 随着更新换代,一个SM中的SP越来越多。
streaming processor(sp): 最基本的处理单元。GPU进行并行计算,也就是很多个sp同时做处理。现在SP的术语已经有点弱化了,而是直接使用thread来代替。一个SP对应一个thread.
Warp:warp是SM调度和执行的基础概念,通常一个SM中的SP(thread)会分成几个warp(也就是SP在SM中是进行分组的,物理上进行的分组),一般每一个WARP中有32个thread.这个WARP中的32个thread(sp)是一起工作的,执行相同的指令,如果没有这么多thread需要工作,那么这个WARP中的一些thread(sp)是不工作的. (每一个线程都有自己的寄存器内存和local memory,一个warp中的线程是同时执行的,也就是当进行并行计算时,线程数尽量为32的倍数,如果线程数不上32的倍数的话;假如是1,则warp会生成一个掩码,当一个指令控制器对一个warp单位的线程发送指令时,32个线程中只有一个线程在真正执行,其他31个 进程会进入静默状态。)
tensor core 和 cuda core 的概念
用户代码 -> AI框架(PyTorch/Tensorflow/Caffe等)-> CUDA lib -> Driver -> 显卡
tensor core和cuda core 都是运算单元,是硬件名词,其主要的差异是算力和运算场景。 NVIDIA的发展历史上来讲,先是推出的cuda core,然后再推出的tensor core 场景:cuda core是全能通吃型的浮点运算单元,tensor core专门为深度学习矩阵运算设计。 算力:在高精度矩阵运算上 tensor cores吊打cuda cores。
CUDA编程
在CUDA编程中,有几种常见的数据类型,包括但不限于:
- 基本数据类型:与C/C++中的基本数据类型相似,包括整型(如int、unsigned int、long long等)、浮点型(如float、double等)、字符型(char)等。
- 向量类型:CUDA提供了一些向量类型,如char1, uchar1, short1, ushort1, int1, uint1, long1, ulong1, float1, double1等,这些类型表示包含一个元素的向量。
- 二维和三维向量类型:类似地,CUDA也提供了二维和三维向量类型,如char2, uchar2, short2, ushort2, int2, uint2, long2, ulong2, float2, double2等,以及对应的三维向量类型。
- 复数类型:CUDA还提供了复数类型,如float_complex和double_complex,用于处理复数运算。
- 半精度浮点数类型:除了常见的浮点数类型外,CUDA还引入了半精度浮点数类型half和half2,用于处理半精度浮点数,以提高内存利用率和计算效率
- dim3是NVIDIA的CUDA编程中一种自定义的整型向量类型,基于用于指定维度的uint3
CUDA的核函数(也称为设备函数)中自动定义的变量:
- threadIdx
- 这是一个三维向量(threadIdx.x, threadIdx.y, threadIdx.z),它指定了当前线程在其线程块中的索引。由于线程块是三维的,所以你可以通过这个索引来定位线程在X、Y、Z的位置
- blockIdx
- 这也是一个三维向量(blockIdx.x, blockIdx.y, blockIdx.z),它指定了当前线程块在整个网格中的索引。这可以让你知道当前线程块在网格的哪个位置
- blockDim
- 这是一个三维向量(blockDim.x, blockDim.y, blockDim.z),它表示线程块的维度,即每个维度上有多少线程。这个值是在启动核函数时由程序员指定的
- gridDim
- 这也是一个三维向量(gridDim.x, gridDim.y, gridDim.z),它表示整个网格的维度,即每个维度上有多少线程块。这个值也是在启动核函数时由程序员指定的
__global__ void kernelFunction(int *data) {
// 计算当前线程的全局索引
int index = blockIdx.x * blockDim.x + threadIdx.x;
// 使用全局索引来访问数组中的元素
data[index] = ...; // 执行某些操作
}
// 根据实际问题来设置blockdim和griddim
dim3 blocksPerGrid(4, 1, 1); // 网格维度:4个线程块
dim3 threadsPerBlock(256, 1, 1); // 线程块维度:每个块256个线程
kernelFunction<<<blocksPerGrid, threadsPerBlock>>>(data); // <<<blocksPerGrid, threadsPerBlock>>>语法是用于在CUDA中启动核函数的特殊语法。这里,blocksPerGrid和threadsPerBlock分别指定了网格和线程块的大小。核函数内部,每个线程会使用它的全局索引来决定操作数组的哪个部分。这样可以确保所有的数组元素都被并行处理,而且每个元素只被处理一次。
CUDA Library
NVIDIA HPC SDK 生态
- libcu++: C++标准库
- https://github.com/nvidia/libcudacxx
- CuBLAS: 线性代数库
- https://docs.nvidia.com/cuda/cublas/index.html
- cutensor
- https://docs.nvidia.com/cuda/cutensor/index.html
- NCCL: 实现多GPU卡的collective communication通信(all-gather, reduce, broadcast)库
- https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/index.html
- CuDNN: 深度学习库
- https://docs.nvidia.com/deeplearning/cudnn/developer-guide/index.html
- TensorRT: 推理优化库
- https://docs.nvidia.com/deeplearning/tensorrt/quick-start-guide/index.html
- DALI: 数据ETL预处理库(py库)
- https://docs.nvidia.com/deeplearning/dali/user-guide/docs/index.html
- cutlass
- 新一代加速库,它是一种模板库,旨在提供通用的GPU计算加速功能
NVIDIA Nsight 产品家族
- nsight system是一个全方位的系统性能监控
- 本地安装 https://developer.download.nvidia.cn/devtools/nsight-systems/
- nsight compute 是CUDA核性能细节展开
- nsight graphics 是图像渲染性能分析展开
GPU 硬件架构
CPU 被设计为以尽可能快的速度执行称为线程(thread)的一系列操作,并且可以并行执行几十个这样的线程; GPU 被设计为并行执行数千个线程(摊销较慢的单线程性能以实现更大的吞吐量), 将更多的晶体管用于数据处理而不是数据缓存和流量控制。
CPU 一般是基于时间片轮转调度原则,每个线程固定地执行单个时间片;而 GPU 的策略则是在线程阻塞的时候迅速换入换出.
CPU
- Core 微架构
- Nehalem
- SandyBridge
- IvyBridge
- Haswell
- Skylake
连接总线
Intel
- FSB (Front Side Bus) 总线:
- 北桥高速,南桥低速
- QPI(Quick Path Interconnect)直连式总线
- DMI(Direct Media Interface) 直接媒体接口
AMD
- HTLink(HyperTransport)
-
Infinity Fabric
- PCI-E (Peripheral Communications Interconnect Express)
- 2010年PCIE3.0传输速度5GB, 2021年6.0传输速度64G
- 频宽
- NVLINK
GPU
截止2021年,按时间线顺序的8代NVIDIA GPU微架构:
- Tesla: 代表是G80, 第一款支持 C 语言的 GPU, 使用标量线程处理器的 GPU,无需程序员手动管理向量寄存器
- Fermi:
- Kepler
- Maxwell
- Pascal: 简称P系列
- Volta: 简称V系列
- Turing: 简称T系列
- Ampere: 简称A系列
GPU 算力演进
Nvidia GPU 算力(compute-capabilities)由版本号表示,有时也称为“SM 版本”。 这个版本号被用来标识 GPU 硬件支持的功能,并在运行时由应用程序使用以确定当前 GPU 上可用的硬件功能和/或指令。 算力包括主要修订号 X 和次要修订号 Y,并用 X.Y 表示。 具有相同主要修订号的设备具有相同的核心架构:
- 基于 Ampere 架构的设备主要修订号为 8; 支持bfloat16、异步编程
- 基于 Volta 架构和 Turing 架构的设备都为 7;引入Independent Thread Scheduling among threads in a warp
- 基于 Pascal 架构的设备为 6;支持64位浮点数的原子操作
- 基于 Maxwell 架构的设备为 5;支持fp16
- 基于 Kepler 架构的设备为 3;
- 基于 Fermi 架构的设备为2 ;引入一级、二级缓存
- 基于 Tesla 架构的设备为1; 与之相对应的是NVIDIA的架构形态:https://en.wikipedia.org/wiki/Category:Nvidia_microarchitectures
怎么查询GPU的算力版本?compute capability
$ nvidia-smi --query-gpu=compute_cap --format=csv
参考
- CUDA C++ Programming Guide
- NVIDIA GPU架构梳理
- CUDA的四种主机存储访问方式
- CUDA 等级概念
- GPU 技术大会
- GPGPU
- OpenMP最新标准
- GPU 架构理解
- NVIDIA GPU 架构演进
- Intel 处理器架构演进
ARM NEON 指令集
ARM从v7版本开始引入高级SIMD,称之为NEON。GCC里面有个编译选项是-fpu=neon,当这个设置时,编译器就支持了ARM SIMD,也就是neon。
GPU 硬编码
GPU 硬编码 实现 AES、H264