深入了解汽车系统级芯片SoC连载之九:详解GPGPU与人工智能
2022-05-18 来源:佐思产研
GPGPU即通用计算GPU,实际不是通用计算,而是深度学习计算或者说AI人工智能计算,这种计算深入到最底层就是矩阵之间的乘积累加。GPGPU基本上被英伟达垄断,英伟达之所以能垄断这个市场,并在无人驾驶领域大放异彩,主要是靠2005年开发的CUDA。
对于GPGPU即通用计算型GPU,软件栈是其生态系统得以扩展的核心。GPGPU在软件栈上主要有两个特点:一是掩盖硬件细节,高度透明,让硬件抽象为一个软件系统,让程序员感觉不到硬件;二是编程模型更友好,人人都能上手。这会产生一个问题,抽象掩藏掉的硬件细节越多,编程模型对用户越友好,那么它会越难充分发挥硬件的全部潜力。因此GPGPU的抽象是分层次的:越靠近用户的层次越易用,同时该层次的性能或者灵活性会越差。这样特定应用领域的用户,如果重心在开发效率,可以选择高层次的编程模型;而需要充分发挥GPGPU性能潜力的用户可以选择低层次的编程模型。这也正是我们把GPGPU的编程模型称为‘软件栈’的原因。
英伟达的GPGPU帝国其核心不是其GPU架构,而是5层软件栈打造的无可匹敌的生态系统,想要打破英伟达的垄断必须也打造类似的生态系统,这难比登天,做硬件容易,GPGPU的IP唾手可得,舍得砸钱有办法在台积电那里拿到最先进的产能,堆核心面积可以轻松做出来比肩英伟达的高性能GPU,但是生态体系没有15年时间是做不起来的。
英伟达的AI软件栈自底向上至少可以分成5层:
SASS是硬件实际执行的指令集,类似CPU的汇编,处在最底层;
PTX是虚拟指令集,为不同代的NVIDIA GPGPU提供了一个统一的编程接口,处在倒数第二层;
CUDA是用户在编写高性能GPGPU程序时主要的编程模型,处在倒数第三层,当然这三层也可以理解为广义的CUDA;
cuBLAS,cuDNN, cuFFT, CUTLASS等运算库勉强算第四层,让用户可以通过调用NVIDIA针对自家GPGPU高度定制的算子库,不需要花费太多精力进行性能调优就可以发挥英伟达GPGPU的最强性能,所有主流深度学习框架(Framework)都集成了这些算子库中的最少一个算子(cuDNN基本都有);
TensorRT、Triton和Megastron则是英伟达针对特定AI应用场景深度定制,让AI类用户开箱即用的软件平台。
人工智能的运算流程通常是用户用CUDA编译代码,编译出架构的PTX指令,再由PTX指令转化为底层的SASS指令,英伟达开放了PTX指令接口,程序员可以使用ASM写PTX的内嵌汇编,SASS没有开放,也不会开放,那是英伟达的核心,SASS应该是能和底层二进制或十六进制的计算机指令一一对应的指令。
CUDA
CUDA是我们接触最多的,CUDA已形成了事实上的垄断,AI或者说人工智能深度学习等高密度计算都离不开CUDA,反过来CUDA也让英伟达的GPGPU热卖,形成滚雪球的良性循环,让英伟达的GPGPU也形成了垄断。要想做GPGPU硬件,就必须兼容CUDA,但这就意味着性能没有优化,这样做出来的模型,在英伟达的设备上自然会运行的比较好。
没有CUDA之前,GPU只能按照图形渲染管线和API来执行操作。CUDA(Compute Unified Device Architecture),它包含了CUDA指令集架构(ISA)以及GPU内部的并行计算引擎。CUDA架起了一座普通程序员与GPU硬件间的桥梁,一方面让GPU可以从事通用计算而不仅仅是图形渲染,且能最大限度发挥GPU多核心的物理优势,另一方面GPU对程序员来说完全透明,程序员可以像用CPU那样为其开发程序。开发人员可以使用最常见的计算机语言即C语言来为CUDA架构编写程序,所编写出的程序可以在支持CUDA的处理器得以最大化发挥其性能。CUDA3.0 开始支持C++和FORTRAN。而这些语言一开始都是为CPU而设计的。
经常有人拿OpenCL与CUDA对比,其实CUDA和OpenCL的关系并不是冲突关系,而是包容关系。CUDA是一个并行计算的架构,包含有一个指令集架构和相应的硬件引擎。OpenCL是一个并行计算的应用程序编程接口(API),CUDA高于OpenCL,它是支持OpenCL的,在NVIDIA CUDA架构上OpenCL是除了C for CUDA外新增的一个CUDA程序开发途径。CUDA C语言与OpenCL的定位不同,或者说是使用人群不同。CUDA C是一种高级语言,那些对硬件了解不多的非专业人士也能轻松上手;而OpenCL则是针对硬件的应用程序开发接口,它能给程序员更多对硬件的控制权,相应的上手及开发会比较难一些。那些在X86 CPU平台使用C语言的人员,会很容易接受基于CUDA GPU平台的C语言;而习惯于使用ARM平台的程序员,看到OpenCL会更加亲切一些,在其基础上开发与图形、视频有关的计算程序会非常容易。基于C语言的CUDA被包装成一种容易编写的代码,因此即使是不熟悉芯片构造的科研人员,也可能利用CUDA工具编写出实用的程序。而OpenCL虽然句法上与CUDA接近,但是它更加强调底层操作,因此难度较高,但正因为如此,OpenCL才能跨平台运行。CUDA最初就是为科研人员开发的。
OpenCL开发的过程中,技术平台均为NVIDIA的GPU,实际上OpenCL是基于NVIDIA GPU的平台进行开发的,当然它是可以跨平台的。另外OpenCL的第一次演示也是运行在NVIDIA的GPU上。从本质上来说,OpenCL就是一个相当于Windows平台中DirectX那样的技术。或者说,它是一个连接硬件和软件的API接口。在这一点上,它与OpenGL类似,不过OpenCL的涉及范围要比OpenGL大得多,它不仅是用来作用于3D图形。如果用一句话描述,OpenCL的作用就是通过调用处理器和GPU的计算资源,释放硬件潜力,让程序运行得更快更好。顺便说一下,OpenCL是苹果在2008年牵头发起的。
CUDA虽然是英伟达独家开发,但其源头却是微软,2005年底,微软推出DirectX 10,DirectX 10最大的革新就是统一渲染架构(Unified Shader Architecture)。各类图形硬件和API均采用分离渲染架构,即顶点渲染和像素渲染各自独立进行,前者的任务是构建出含三维坐标信息的多边形顶点,后者则是将这些顶点从三维转换为二维。这为实现GPU通用计算奠定了基础。第一代CUDA在2006年底发布的首款DX10规范的G80架构上实现,到了GT 200时代,GTX 200系列显卡实现了硬件级双精度算术(GT200核心中拥有30个64位浮点单元)。
英伟达的显卡架构都有对应的CUDA版本,比如3.x称为Kepler,包括3.0、3.5、3.7等,5.x称为Maxwell,包括5.0、5.2、5.3等,6.x是Pascal,包括6.0、6.1、6.2等。7.0是Volta,7.5是Turing。目前最常见的Ampere架构卡A100是8.0,目前Ampere架构最高到11.6版,最新的Hopper架构应该是12.x版。随着英伟达在CPU领域的持续发力,未来CUDA可能会有比较大的变化。
CUDA架构
图片来源:互联网
CUDA主要提供了4个重要的东西:CUDA C和对应的COMPILER,CUDA库、CUDA RUNTIME和CUDA DRIVER。CUDA C其实就是C的变种,它加入4大特性:1)可以定义程序的哪部分运行在GPU或CPU上;2)可以定义变量位于GPU的存储类型;3)利用KERNEL、BLOCK、GRID来定义最原始的并行计算;4)State变量。CUDA库包含了很多有用的数学应用,如cuFFT,CUDA RUNTIME其实就是个JIT编译器,动态地将PTX中间代码编译成符合实际平台的硬件代码,并做特定优化。Driver便是相应API直接与GPU打交道的接口了。
在CUDA中程序执行区域分为两部分,CPU和GPU——HOST和DEVICE,任务组织和发送是在CPU里完成的,但并行计算是在GPU里完成,每当CPU遇到需要并行计算的任务,则将要做的运算组织成kernel,即数据并行处理函数(核函数),然后发给GPU去执行,在GPU上执行的程序,一个Kernel对应一个Grid网格,网格是一维或多维线程块(block),CUDA在把任务正式提交给GPU前,会对kernel做些处理,让kernel符合GPU体系架构,把GPU想成拥有上百个核的CPU,kernel当成一个要创建为线程的函数,所以CUDA现在要用kernel创建出上百个thread,然后将这些thread送到GPU中的各个核上去运行,为了更好利用GPU资源,提高并行度,CUDA还要将这些thread加以优化组织,将能利用共有资源的线程组织到一个thread block中,同一thread block中的thread可以共享数据,每个thread block最高可拥有512个线程。拥有同样维度同样kernel的thread block被组织成一个grid,而CUDA处理任务的最大单元便是grid了。
人工智能、机器学习、深度学习基础
所谓人工智能实际可以等同于机器学习,人是能够提出问题发现问题的,而机器永远做不到这一点,从这个角度讲,人工智能永远都无法实现,因为人类一切科学的源头,智能的源头就是人类有好奇心,能够发现问题,而机器只能解决问题。
机器学习中最常见的是深度学习,深度学习可以按照训练方式分为六大类,分别是:
无监督学习(unsupervised learning):已知数据没有任何标注,按照一定的偏好,训练一个智能算法,将所有的数据映射到多个不同标签的过程。
强化学习(reinforcement learning):智能算法在没有人为指导的情况下,通过不断的试错来提升任务性能的过程。“试错”的意思是还是有一个衡量标准,用棋类游戏举例,我们并不知道棋手下一步棋是对是错,不知道哪步棋是制胜的关键,但是我们知道结果是输还是赢,如果算法这样走最后的结果是胜利,那么算法就学习记忆,如果按照那样走最后输了,那么算法就学习以后不这样走。
弱监督学习(weakly supervised learning):已知数据和其一一对应的弱标签,训练一个智能算法,将输入数据映射到一组更强的标签的过程。标签的强弱指的是标签蕴含的信息量的多少,比如相对于分割的标签来说,分类的标签就是弱标签。
半监督学习(semi supervised learning) :已知数据和部分数据一一对应的标签,有一部分数据的标签未知,训练一个智能算法,学习已知标签和未知标签的数据,将输入数据映射到标签的过程。半监督通常是一个数据的标注非常困难,比如说医院的检查结果,医生也需要一段时间来判断健康与否,可能只有几组数据知道是健康还是非健康。
多示例学习(multiple instance learning) :已知包含多个数据的数据包和数据包的标签,训练智能算法,将数据包映射到标签的过程,在有的问题中也同时给出包内每个数据的标签。多事例学习引入了数据包的概念。
监督学习(supervised learning):已知数据和其一一对应的标注(标签),也就是说训练数据集需要全部标注。训练一个智能算法,将输入数据映射到标注的过程。监督学习是最常见的深度学习,也是ADAS自动驾驶感知领域几乎唯一的深度学习方式,就是人们口中常说的分类(Classification)问题。
图片来源:互联网
深度学习分为训练和推理两部分,训练就好比我们在学校的学习,但神经网络的训练和我们人类接受教育的过程之间存在相当大的不同。神经网络对我们人脑的生物学——神经元之间的所有互连——只有一点点拙劣的模仿。我们的大脑中的神经元可以连接到特定物理距离内任何其它神经元,而深度学习却不是这样——它分为很多不同的层(layer)、连接(connection)和数据传播(data propagation)的方向,因为多层,又有众多连接,所以称其为神经网络。
训练神经网络的时候,训练数据被输入到网络的第一层。然后所有的神经元,都会根据任务执行的情况,根据其正确或者错误的程度如何,分配一个权重参数(权重值)。在一个用于图像识别的网络中,第一层可能是用来寻找图像的边。第二层可能是寻找这些边所构成的形状——矩形或圆形。第三层可能是寻找特定的特征——比如闪亮的眼睛或按钮式的鼻子。每一层都会将图像传递给下一层,直到最后一层;最后的输出由该网络所产生的所有这些权重总体决定。
经过初步(是初步,这个是隐藏的)训练后得到全部权重模型后,我们就开始考试它,比如注入神经网络几万张含有猫的图片(每张图片都需要在猫的地方标注猫,这个过程一般是手工标注,也有自动标注,但准确度肯定不如手工),然后拿一张图片让神经网络识别图片里的是不是猫。如果答对了,这个正确会反向传播到该权重层,给予奖励就是保留,如果答错了,这个错误会回传到网络各层,让网络再猜一下,给出一个不同的论断,这个错误会反向地传播通过该网络的层,该网络也必须做出其它猜测,网络并不知道自己错在哪里,也无需知道。在每一次尝试中,它都必须考虑其它属性——在我们的例子中是「猫」的属性——并为每一层所检查的属性赋予更高或更低的权重。然后它再次做出猜测,一次又一次,无数次尝试……直到其得到正确的权重配置,从而在几乎所有的考试中都能得到正确的答案。
得到正确的权重配置,这是一个巨大的数据库,显然无法实际应用,特别是嵌入式应用,于是我们要对其修剪,让其瘦身。首先去掉神经网络中训练之后就不再激活的部分。这些部分已不再被需要,可以被「修剪」掉。其次是压缩,这和我们常用的图像和视频压缩类似,保留最重要的部分,如今模拟视频几乎不存在,都是压缩视频的天下,但我们并未感觉到压缩视频与原始视频有区别。压缩的理论基础是信息论(它与算法信息论密切相关)以及率失真理论,这个领域的研究工作主要是由上世纪40年代的 Claude Shannon 奠定的,实际机器学习所有的理论基础在上世纪50年代就已经全部具备,绝大部分理论基础也来自Claude Shannon 的信息论,唯一差的就是算力,是英伟达的GPU造就了深度学习时代的到来,目前的深度学习没有理论上的突破,只是应用上的扩展。经过压缩后,多个神经网络层被合为一个单一的计算。最后得到的这个就是推理Inference用模型或者说算法模型,实际我觉得叫Prediction猜测更准确。
图片来源:互联网
深度学习的关键理论是线性代数和概率论,因为深度学习的根本思想就是把任何事物转化成高维空间的向量,强大无比的神经网络,说来归齐就是无数的矩阵运算和简单的非线性变换的结合。在19世纪中期,矩阵理论就已经成熟。概率论在18世纪中期就有托马斯贝叶斯,在1900年俄罗斯的马尔科夫发表概率演算,概率论完全成熟。优化理论主要来自微积分,包括拉格朗日乘子法及其延伸的KKT,而拉格朗日是18世纪中叶的法国数学家。RNN则和非线性动力学关联甚密,其基础在20世纪初已经完备。至于GAN网络,则离不开19世纪末伟大的奥地利物理学家波尔兹曼。强化学习的理论基础是1906年俄罗斯数学家马尔科夫发表的弱大数定律(weak law of large numbers)和中心极限定理(central limit theorem),也就是马尔科夫链。
深度学习的理论基础已经不可能出现大的突破,因为目前人类的数学特别是非确定性数学已经走火入魔了。
实际深度学习就是靠蛮力计算(当然也有1X1卷积、池化等操作降低参数量和维度)代替了精妙的科学。深度学习没有数学算法那般有智慧,它知其然,不知其所以然,它只是概率预测(深度学习里最重要的置信度)。所以在目前的深度学习方法中,参数的调节方法依然是一门“艺术”,而非“科学”。深度学习方法深刻地转变了人类几乎所有学科的研究方法。以前学者们所采用的观察现象,提炼规律,数学建模,模拟解析,实验检验,修正模型的研究套路被彻底颠覆,被数据科学的方法所取代:收集数据,训练网络,实验检验,加强训练。这也使得算力需求越来越高。机械定理证明验证了命题的真伪,但是无法明确地提出新的概念和方法,实质上背离了数学的真正目的。这是一种“相关性”而非“因果性”的科学。历史上,人类积累科学知识,在初期总是得到“经验公式”,但是最终还是寻求更为深刻本质的理解。例如从炼丹术到化学、量子力学的发展历程。人类智能独特之处也在于数学推理,特别是机械定理证明,对于这一点,机器学习永远无能为力的。
对于深度学习推理阶段来说,分解到最底层,其运算核心就是数据矩阵与权重模型之间的乘积累加,乘积累加运算(英语:Multiply Accumulate, MAC)。这种运算的操作,是将乘法的乘积结果和累加器A的值相加,再存入累加器:
若没有使用MAC指令,上述的程序需要二个指令,但MAC指令可以使用一个指令完成。而许多运算(例如卷积运算、点积运算、矩阵运算、数字滤波器运算、乃至多项式的求值运算,基本上全部的深度学习类型都可以对应)都可以分解为数个MAC指令,因此可以提高上述运算的效率。推理阶段要求精度不高,一般是整数8位,即INT8。
对于训练阶段,要求比较高,常见的是FP64和FP32两种精度,近来又出现Bfloat16,Bfloat16就是截断浮点数(truncated 16-bit floating point),它是由一个float32截断前16位而成。它和IEEE定义的float16不同,主要用于取代float32来加速训练网络,同时降低梯度消失(vanishing gradient)的风险,也可以防止出现NaN这样的异常值。深层神经网络每次梯度相乘的系数如果小于1,那就是浮点数,如果层数越来越多,那这个系数会越来越大,传播到最底层可以学习到的参数就很小了,所以需要截断来防止(或降低)梯度消失。在float32和bfloat16之间进行转换时非常容易,事实上 Tensorflow也只提供了bfloat16和float32之间的转换,不过毕竟还是需要转换的。
英特尔的内嵌汇编格式GNU Gas添加了BFloat16支持,英特尔在2019年4月发布补丁,支持GNU编译器集合(GCC)中的BFloat16支持。和IEEE float16相比,动态范围更大(和float32一样大),但是尾数位更少(精度更低)。更大的动态范围意味着不容易下溢(上溢在实践中几乎不会发生,这里不考虑)。另一个优势是Bfloat16既可以用于训练又可以用于推断。Amazon也证明Deep Speech模型使用BFloat16的训练和推断的效果都足够好。Uint8在大部分情况下不能用于训练,只能用于推断,大多数的Uint8模型都从FP32转换而来。所以,Bfloat16可能是未来包括移动端的主流格式,尤其是需要语言相关的模型时候。当然英伟达认为Bfloat16牺牲了部分精度,对于某些场合如HPC,精度比运算效率和成本更重要。
一个MAC单元通常包括三部分:乘法器、加法器和累加器。
图片来源:互联网
上图为一个典型的MAC单元。计算机体系中的乘法和加法都历经了长时间的研究改进,纯粹的乘法器和加法器肯定是不会有人用。乘法器最常用的Wallace树,这是1963年C.S.Wallace提出的一种高效快速的加法树结构,被后人称为Wallace树。人工智能95%的理论工作都是在1970年前完成的,只是没有高性能计算系统,才没有在那个时代铺展开。加法器多是CSA,即进位保存加法器(Carry Save Adder,CSA)。使用进位保存加法器在执行多个数加法时具有极小的进位传播延迟,它的基本思想是将3个加数的和减少为2个加数的和,将进位c和s分别计算保存,并且每比特可以独立计算c和s,所以速度极快。这些都已经非常成熟,刚出校门的学生都可以做到。
除了降低精度以外,还可以结合一些数据结构转换来减少运算量,比如通过快速傅里叶变换(FFT)来减少矩阵运算中的乘法;还可以通过查表的方法来简化MAC的实现等。