在前不久的 Baidu Create 2019 百度 AI 开发者大会上,Apollo 发布了业内首创的 AVP 专用车载计算平台——百度 AVP 专用量产计算单元 ACU-Advanced。
本篇文章,我们将从与自动驾驶的关系、加速中遇到的挑战、量化计算、节约资源和带宽五个方面,介绍 ACU-Advanced 的核心高性能芯片 FPGA 的相关技术。
这是一篇“硬核”的技术文章。正是这些后台的“硬核”技术,成就了令人炫目的自动驾驶。本文中介绍的相关技术已经落实在 Valet Parking 产品中的量产 ACU 硬件上。
自动驾驶与 FPGA
人工智能技术是自动驾驶的基础,算法、算力和数据是其三大要素。本文探讨的就是其中的“算力”。算力的高低,不仅直接影响了行驶速度的高低,还决定了有多大的信息冗余用来保障驾驶的安全。
算力最直观地体现在硬件上,而汽车对自动驾驶的控制器有特殊的要求。
除了对一般硬件的成本、体积重量、功耗的要求外,还要求:
提供足够的算力,保证行驶速度和信息冗余。
满足严苛的车规标准,比如超宽的温度范围,-40℃ – 85℃。
综合来看 FPGA 是适合自动驾驶高速计算的技术,它具有以下的突出优点:
技术可靠。FPGA 在汽车行业早已被广泛使用,也经受了军工、航天、通信、医疗等需要高可靠性行业的考验。相比而言,GPU 不具有这个特点,而为自动驾驶新开发的 ASIC 尚需时间检验。
灵活,有利于算法迭代。FPGA 具有可编程的特点,尤其适合自动驾驶这种新兴的、功能需求并不完全确定的行业。如果使用 ASIC,则算法的自由度就被束缚,不利于算法的演进。ASIC 开发周期需要几年,如果采用 ASIC 加速,算法理念被锁定在几年前。比如,如果 ASIC 被设计为只能为 CNN 加速,那基于规则的立体视觉技术将无法实现。即便 ASIC 设计中考虑了双目立体视觉的加速,那基于运动的立体视觉技术(Structure From Motion, SFM)就无法实现。这些繁复的、变化的需求是新兴产业的标志,但也使 ASIC 很难完全承接。
有成品可用。已经有成熟的 FPGA 产品,提供不同的算力,可以直接选择。新的 ASIC 开发延期,甚至失败,并不是小概率事件。笔者曾经在通信设备行业工作10年,见证了移动通信技术 2G、3G、4G、5G 的变迁。即便通信行业标准清晰且超前,为排除技术不确定性,每次技术变迁时,总是先推出基于 FPGA 的量产产品,确保可以占领市场先机。
尽管 FPGA 有可靠、灵活、有成熟成品的优点,但 FPGA 的开发有很强的专业性,最终实现的效果与具体的设计很相关。
FPGA 加速遇到的挑战
实践中遇到的挑战是,多种多样的加速需求和有限的硬件资源的矛盾。
需求的来源既包括深度学习前向推测、也包括基于规则的算法。
硬件资源受限包括了:FPGA 资源受限和内存带宽受限。
FPGA 资源的有限性体现:
峰值算力受限:有限的 FPGA 资源限制了计算并行度的提高,这约束了峰值算力。
支持的算子种类受限:有限的 FPGA 资源只能容纳有限个算子。
内存带宽受限体现在:
内存数据传输在计算总时间中占据了不可忽略的时间。
极端情况下,对某些算子提高并行度后,计算时间不减。
为应对这些挑战,我们在实践中提取了一些有益的经验,总结出来与大家共享。
量化计算
算法工程师采用浮点数 float32 对模型进行训练,产出的模型参数也是浮点型的。然而在我们使用的 FPGA 中,没有专用的浮点计算单元,要实现浮点数计算,代价很大,不可行。使用 int8 计算来逼近浮点数计算,也即实现量化计算,这是需要解决的第一个问题。
量化计算原理
以矩阵 C = A*B 为例,假设 A、B 元素为 float32 类型,采用爱因斯坦标记法:
符号表示四舍五入,两个把矩阵A和B的元素线性映射到区间[-127, 127],在此区间完成乘法和加法。最后一个乘法把整型结果还原成 float32。
假设 i = j = k =100:
在量化前,需要完成1000000次 float32 的乘法。
量化成 int8 后,需要完成1000000次 int8 的乘法,和30000次量化、反量化乘法。
由于量化和反量化占的比重很低,量化的收益就等于 int8 取代 float32 乘法的收益,这是非常显著的。
未知量化尺度:动态量化
如果上面式子中,量化尺度 max|A|, max|B|,在计算前是未知的,每次计算矩阵乘法前,就需要逐个查找 A 和 B 的元素,找出量化尺度。
这种方法的好处是,每次计算既能充分利用 int8 数据的表征能力(127总能被使用到),不存在数据饱和的情况(所有元素都被线性映射),保证单次计算的精度最高。可以直接接受浮点训练的模型,维持准召率。Resnet50 测50000张图片,Top1 和 Top5 准确率下降1%。在 Valet Parking 产品用到的多个网络中,没有观察到准召率下降。
缺点是,FPGA 计算有截断误差,经过多次累计,数值计算误差最大平均可以达到10%。对于一些训练不完全成功的模型(只在有限评测集上效果比较好),准召率下降明显,结果不可控。
已知量化尺度:静态量化
如果上面的式子变成
经过线下统计,量化尺度被固化为 scaleA 和 scaleB, 表示四舍五入,并且限制在[-127, 127]之内。
这种方法的好处是
节约了 FPGA 资源。
可以很方便地采用跟量化推测一致的训练方法,推测和训练计算数值误差很小,准召率可控。
缺点是,要求模型训练采用一致的量化方法。否则,计算误差很大,不可接受。
节约 FPGA 资源
共享 DMA 模块
FPGA 片上存储非常受限,对于绝大多数的算子,不可能将输入或者输出完整缓存到片上内存中。而是从内存中一旦读取足够的数据,就开始计算。一旦计算到足够多,就立即把结果写到内存。和内存数据的流式交互是个公共的需求,我们开发了能兼顾所有算子的 DMA 的接口。
只有对于单次计算耗时很长、或调用非常频繁的独立任务算子,我们才为其定制单独 DMA 的模块,取得的收益是,这个算子可以通过多线程调度和其它 FPGA 算子并行计算。这是综合收益和代价后,做出的以资源换时间的折衷。
采用 SuperTile 结构
Int8 的计算,可以使用 DSP 或其它逻辑资源来完成。逻辑资源有更多的用途,所以我们占用 DSP 来完成 int8 的乘累加计算。FPGA 内部的 DSP48E2 可以接受 27bit 的乘数。可以把两个 int8 的乘数排列在高 8bit 和低 8bit,进行一次乘法后,再两个乘积完整的分离出来。这样,就实现了单个 DSP 一个时钟周期完成了两个乘法,达到了算力倍增的效果。
算子资源复用
通过观察和抽象,将 CNN 主要的算子抽象成3类:
指数类算子
单通道算子
多通道算子
实现了每一类共享计算资源,大大节约了 FPGA 资源的占用,为提高峰值算力、和支持更多的算子提供了有利条件。
经过观察
而对于两通道的 softmax,它把两个数 a, b 映射成两个概率,且 Pa + Pb = 1,计算法则是:
指数计算在 FPGA 中是比较消耗资源的,通过把 tanh 和 softmax 化成 sigmoid 的形式,我们就实现了一份指数运算资源,支持3种算子。
Average pooling 可以视为固定卷积核的 depthwise conv。
可以构造额外的卷积核,在上层 SDK 把 average pooling 封装成 depthwise conv 直接计算,这样 FPGA 无需做任何兼容设计,节约 FPGA 资源。
也可以在 RTL 代码中完成转换,这样不需要传递卷积核参数,节约内存带宽。
Elementwise add 的计算形式是两个输入、一个输出,而 depthwise conv 的计算形式是一个输入、一个输出。二者计算资源的复用并不显然。我们操作两个输入向 FPGA 加载的顺序,加载数据的同时完成了两个输入特征图的按行交织,将两个输入交织成一个输入。然后在 RTL 中构造一个[1, 1] T 的卷积核,stride 设置为[1, 2],变成 depthwise conv 的计算形式,利用 depthwise 的计算资源完成计算。
我们在设计 elementwise add 的时候,抽象度比较高,超出了原始定义的 A + B,扩展成 mA + nB。 其中 m、n 是 SDK 可以自由配置的参数,当m = n = 1,回归到传统的 elementwise add。而取 m = 1,n = -1 时,完成的是 elementwise sub。在 FPGA 无感的情况下,实现了 elementwise add 和 elementwise sub 计算资源的复用。
综上, depthwise convolution, average pooling, elementwise add , elementwise sub 这四种单通道的算子计算资源是复用的。
多通道算子资源的复用,只介绍最关键的乘累加部分。
conv 实现的是3维输入图像(H x W x C)和4维卷积核(N x K1 x K2 x C)的乘加操作。full connection 实现的1维输入数组(长度是C)和2维权重(N x C)的乘加操作。将 full connection 输入数据扩维,输入数组扩展成 H x W x C, 输出扩展成 N x K1 x K2 x C, 其中 H = W = K1 = K2 = 1。 这样 full connection 就被 SDK 封装成了 conv,FPGA 计算时无感。
Deconv 和 conv 是网络中计算量最大的两个算子,计算资源复用收益很大。但它们计算形式上差别很大,直接复用计算资源很困难。我们在理论上进行突破,实现了通用的资源复用的方法。简言之,SDK 要对 conv 的计算参数进行扩充以兼容 deconv,在计算 deconv 时,需要对卷积核进行分拆、重排,伪装成 conv。FPGA 计算完毕后,增加少量逻辑对结果进行修饰。
总结一下,我们对 CNN 常用的十种算子抽象,只花费三个算子的资源。
异构计算
ARM 计算:有些算子,比如多通道的 softmax、concat、split 等,出现频率很低,数据量不大,对整体帧率影响很小,还有些算子比如 PSRoiPooling、计算区域不确定、数据不能保证对齐,非常不适合 FPGA 加速。把这两类算子放在 ARM 上实现。在 ARM 上对计算影响最大的单个因素是缓存命中率。通过数据重排、改变遍历顺序等,提高缓存命中率,可以把表观 ARM 算力提高几十倍。
NEON 加速:采用 NEON 指令可以对多通道的 Softmax 算子有效加速,加速比虽然不及 FPGA,但相对于直接采用未优化的 C++ 的代码在 ARM 上执行,效果可以提升数倍。其它对齐的计算,大多可以通过 NEON 处理器加速数倍。
MaliGPU:我们目前使用 Xilinx ZU 系列的 FPGA,自带 MaliGPU 400,原本被设计用来显示时渲染,并不支持 CUDA、OpenCL 等常用库。经过特殊的驱动方式,我们做到可以利用它实现一些受限的逐像素算子。
我们实际计算使用的硬件资源包括了 FPGA、MaliGPU、ARM 主处理器、ARM Neon 协处理器4种。通过 ARM(主处理其和协处理器)和 MaliGPU 实现对部分算子进行承接,有效缓解了 FPGA 的资源压力。
采用静态量化
而采用动态量化,搜索量化尺度和进行量化,需要分散在相邻的两个算子中实现。为了保证精度,中间结果需要以半浮点(float16)形式表示。这带来两个问题:
CPU 并不能直接对 FP16 的数据进行转换或计算,所以需要 FPGA 提供额外的算子,提供快速的 float32 / int8 和 float16 转换。这些额外的算子,是 CNN 本身不需要的,这构成了浪费。
Float16 需要的缓存比 int8 大了一倍。浪费了 FPGA 的存储资源。
而静态量化,离线提供了固定的量化参数,中间计算结果以量化后的 int8 形式来表示。以上的浪费都得以避免。尽管静态量化对模型训练做额外的要求,我们最终决定切换到静态量化。
节约内存带宽
多算子融合
通过 SDK 将各种参数进行变换和合并,单个 conv 算子可以完成最多支持4个算子的组合 conv + batchnorm + scale + relu。
我们还可以将指数型算子(sigmoid / tanh /两通道 softmax)融合到上面4个算子之后,形成融合5个基本算子的单一融合算子。这依赖于自主开发的 SDK,和 FPGA 设计的算子逻辑。
相对于每计算一个算子就把结果回吐给 DDR,这种算子融合大大减少了对内存的读写。有效提高了处理帧率。
静态量化
动态量化中间结果以 float16 表示,而静态量化可以以 int8 形式表示。静态量化相对于动态量化,内存的吞吐量降低,帧率有明显的提高。下图是保持算力不变,仅仅把中间结果从 float16 变成 int8 后,处理帧率的提高幅度。
静态量化降低了内存吞吐,这也是我们放弃动态量化易用性的一个原因。
任务的帧率是峰值算力、各算子算力、支持的算子种类三个因素复合作用的结果。以上技术已经用到了 ACU 硬件中,把百度 Valet Parking 产品的帧率在数量级上进行了提高。
接下来,我们会陆续发布更多这样的“硬核”技术文章,让更多开发者们更加细致地了解 Apollo 自动驾驶背后的技术。
评论
查看更多