跳转到主要内容

自动驾驶中 FPGA 加速的挑战与实践

judy 提交于

<font color="#FF8000">注:本文转载自: Apollo阿波罗智能驾驶微信公众号(微信号:baiduidg)</font>

在前不久的 Baidu Create 2019 百度 AI 开发者大会上,Apollo 发布了业内首创的 AVP 专用车载计算平台——百度 AVP 专用量产计算单元 ACU-Advanced。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76633-1.pn…; alt=""></center>

本篇文章,我们将从与自动驾驶的关系、加速中遇到的挑战、量化计算、节约资源和带宽五个方面,介绍 ACU-Advanced 的核心高性能芯片 FPGA 的相关技术。

这是一篇“硬核”的技术文章。正是这些后台的“硬核”技术,成就了令人炫目的自动驾驶。本文中介绍的相关技术已经落实在 Valet Parking 产品中的量产 ACU 硬件上。

<strong>自动驾驶与 FPGA</strong>

人工智能技术是自动驾驶的基础,算法、算力和数据是其三大要素。本文探讨的就是其中的“算力”。算力的高低,不仅直接影响了行驶速度的高低,还决定了有多大的信息冗余用来保障驾驶的安全。

算力最直观地体现在硬件上,而汽车对自动驾驶的控制器有特殊的要求。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76634-2.pn…; alt=""></center>

除了对一般硬件的成本、体积重量、功耗的要求外,还要求:

<li>提供足够的算力,保证行驶速度和信息冗余。</li>

<li>满足严苛的车规标准,比如超宽的温度范围,-40℃ – 85℃。</li>

综合来看 FPGA 是适合自动驾驶高速计算的技术,它具有以下的突出优点:

<li>技术可靠。FPGA 在汽车行业早已被广泛使用,也经受了军工、航天、通信、医疗等需要高可靠性行业的考验。相比而言,GPU 不具有这个特点,而为自动驾驶新开发的 ASIC 尚需时间检验。</li>

<li>灵活,有利于算法迭代。FPGA 具有可编程的特点,尤其适合自动驾驶这种新兴的、功能需求并不完全确定的行业。如果使用 ASIC,则算法的自由度就被束缚,不利于算法的演进。ASIC 开发周期需要几年,如果采用 ASIC 加速,算法理念被锁定在几年前。比如,如果 ASIC 被设计为只能为 CNN 加速,那基于规则的立体视觉技术将无法实现。即便 ASIC 设计中考虑了双目立体视觉的加速,那基于运动的立体视觉技术(Structure From Motion, SFM)就无法实现。这些繁复的、变化的需求是新兴产业的标志,但也使 ASIC 很难完全承接。</li>

<li>有成品可用。已经有成熟的 FPGA 产品,提供不同的算力,可以直接选择。新的 ASIC 开发延期,甚至失败,并不是小概率事件。笔者曾经在通信设备行业工作10年,见证了移动通信技术 2G、3G、4G、5G 的变迁。即便通信行业标准清晰且超前,为排除技术不确定性,每次技术变迁时,总是先推出基于 FPGA 的量产产品,确保可以占领市场先机。</li>

尽管 FPGA 有可靠、灵活、有成熟成品的优点,但 FPGA 的开发有很强的专业性,最终实现的效果与具体的设计很相关。

<strong>FPGA 加速遇到的挑战</strong>

实践中遇到的挑战是,多种多样的加速需求和有限的硬件资源的矛盾。

需求的来源既包括深度学习前向推测、也包括基于规则的算法。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76635-3.pn…; alt=""></center>

硬件资源受限包括了:FPGA 资源受限和内存带宽受限。

FPGA 资源的有限性体现:

峰值算力受限:有限的 FPGA 资源限制了计算并行度的提高,这约束了峰值算力。

支持的算子种类受限:有限的 FPGA 资源只能容纳有限个算子。

内存带宽受限体现在:

<li>内存数据传输在计算总时间中占据了不可忽略的时间。</li>

<li>极端情况下,对某些算子提高并行度后,计算时间不减。</li>

为应对这些挑战,我们在实践中提取了一些有益的经验,总结出来与大家共享。

<strong>量化计算</strong>

算法工程师采用浮点数 float32 对模型进行训练,产出的模型参数也是浮点型的。然而在我们使用的 FPGA 中,没有专用的浮点计算单元,要实现浮点数计算,代价很大,不可行。使用 int8 计算来逼近浮点数计算,也即实现量化计算,这是需要解决的第一个问题。

<strong>量化计算原理</strong>

以矩阵 C = A*B 为例,假设 A、B 元素为 float32 类型,采用爱因斯坦标记法:
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76636-4.pn…; alt=""></center>

<>符号表示四舍五入,两个<>把矩阵A和B的元素线性映射到区间[-127, 127],在此区间完成乘法和加法。最后一个乘法把整型结果还原成 float32。

假设 i = j = k =100:

<li>在量化前,需要完成1000000次 float32 的乘法。</li>

<li>量化成 int8 后,需要完成1000000次 int8 的乘法,和30000次量化、反量化乘法。</li>

由于量化和反量化占的比重很低,量化的收益就等于 int8 取代 float32 乘法的收益,这是非常显著的。

<strong>未知量化尺度:动态量化</strong>

如果上面式子中,量化尺度 max|A|, max|B|,在计算前是未知的,每次计算矩阵乘法前,就需要逐个查找 A 和 B 的元素,找出量化尺度。

<li>这种方法的好处是,每次计算既能充分利用 int8 数据的表征能力(127总能被使用到),不存在数据饱和的情况(所有元素都被线性映射),保证单次计算的精度最高。可以直接接受浮点训练的模型,维持准召率。Resnet50 测50000张图片,Top1 和 Top5 准确率下降1%。在 Valet Parking 产品用到的多个网络中,没有观察到准召率下降。</li>

<li>缺点是,FPGA 计算有截断误差,经过多次累计,数值计算误差最大平均可以达到10%。对于一些训练不完全成功的模型(只在有限评测集上效果比较好),准召率下降明显,结果不可控。</li>

<strong>已知量化尺度:静态量化</strong>

如果上面的式子变成
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76637-5.pn…; alt=""></center>

经过线下统计,量化尺度被固化为 scaleA 和 scaleB, 表示四舍五入,并且限制在[-127, 127]之内。

这种方法的好处是

<li>节约了 FPGA 资源。</li>

<li>可以很方便地采用跟量化推测一致的训练方法,推测和训练计算数值误差很小,准召率可控。</li>

缺点是,要求模型训练采用一致的量化方法。否则,计算误差很大,不可接受。

<strong>节约 FPGA 资源</strong>

<strong>共享 DMA 模块</strong>

FPGA 片上存储非常受限,对于绝大多数的算子,不可能将输入或者输出完整缓存到片上内存中。而是从内存中一旦读取足够的数据,就开始计算。一旦计算到足够多,就立即把结果写到内存。和内存数据的流式交互是个公共的需求,我们开发了能兼顾所有算子的 DMA 的接口。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76638-6.pn…; alt=""></center>

只有对于单次计算耗时很长、或调用非常频繁的独立任务算子,我们才为其定制单独 DMA 的模块,取得的收益是,这个算子可以通过多线程调度和其它 FPGA 算子并行计算。这是综合收益和代价后,做出的以资源换时间的折衷。

<strong>采用 SuperTile 结构</strong>
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76639-7.pn…; alt=""></center>

Int8 的计算,可以使用 DSP 或其它逻辑资源来完成。逻辑资源有更多的用途,所以我们占用 DSP 来完成 int8 的乘累加计算。FPGA 内部的 DSP48E2 可以接受 27bit 的乘数。可以把两个 int8 的乘数排列在高 8bit 和低 8bit,进行一次乘法后,再两个乘积完整的分离出来。这样,就实现了单个 DSP 一个时钟周期完成了两个乘法,达到了算力倍增的效果。

<strong>算子资源复用</strong>

通过观察和抽象,将 CNN 主要的算子抽象成3类:

<li>指数类算子</li>
<li>单通道算子</li>
<li>多通道算子</li>

实现了每一类共享计算资源,大大节约了 FPGA 资源的占用,为提高峰值算力、和支持更多的算子提供了有利条件。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76640-8.pn…; alt=""></center>

<strong>经过观察</strong>
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76641-9.pn…; alt=""></center>

而对于两通道的 softmax,它把两个数 a, b 映射成两个概率,且 Pa + Pb = 1,计算法则是:
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76642-10.p…; alt=""></center>

指数计算在 FPGA 中是比较消耗资源的,通过把 tanh 和 softmax 化成 sigmoid 的形式,我们就实现了一份指数运算资源,支持3种算子。

Average pooling 可以视为固定卷积核的 depthwise conv。

<li>可以构造额外的卷积核,在上层 SDK 把 average pooling 封装成 depthwise conv 直接计算,这样 FPGA 无需做任何兼容设计,节约 FPGA 资源。</li>

<li>也可以在 RTL 代码中完成转换,这样不需要传递卷积核参数,节约内存带宽。</li>

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 常用的十种算子抽象,只花费三个算子的资源。

<strong>异构计算</strong>
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76643-11.p…; alt=""></center>

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 的资源压力。

<strong>采用静态量化</strong>

而采用动态量化,搜索量化尺度和进行量化,需要分散在相邻的两个算子中实现。为了保证精度,中间结果需要以半浮点(float16)形式表示。这带来两个问题:

<li>CPU 并不能直接对 FP16 的数据进行转换或计算,所以需要 FPGA 提供额外的算子,提供快速的 float32 / int8 和 float16 转换。这些额外的算子,是 CNN 本身不需要的,这构成了浪费。</li>

<li>Float16 需要的缓存比 int8 大了一倍。浪费了 FPGA 的存储资源。</li>

而静态量化,离线提供了固定的量化参数,中间计算结果以量化后的 int8 形式来表示。以上的浪费都得以避免。尽管静态量化对模型训练做额外的要求,我们最终决定切换到静态量化。

<strong>节约内存带宽</strong>

<strong>多算子融合</strong>

通过 SDK 将各种参数进行变换和合并,单个 conv 算子可以完成最多支持4个算子的组合 conv + batchnorm + scale + relu。

我们还可以将指数型算子(sigmoid / tanh /两通道 softmax)融合到上面4个算子之后,形成融合5个基本算子的单一融合算子。这依赖于自主开发的 SDK,和 FPGA 设计的算子逻辑。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76644-12.p…; alt=""></center>

相对于每计算一个算子就把结果回吐给 DDR,这种算子融合大大减少了对内存的读写。有效提高了处理帧率。

<strong>静态量化</strong>

动态量化中间结果以 float16 表示,而静态量化可以以 int8 形式表示。静态量化相对于动态量化,内存的吞吐量降低,帧率有明显的提高。下图是保持算力不变,仅仅把中间结果从 float16 变成 int8 后,处理帧率的提高幅度。
<center><img src="http://xilinx.eetrend.com/files/2019-08/wen_zhang_/100044509-76645-13.p…; alt=""></center>

静态量化降低了内存吞吐,这也是我们放弃动态量化易用性的一个原因。

任务的帧率是峰值算力、各算子算力、支持的算子种类三个因素复合作用的结果。以上技术已经用到了 ACU 硬件中,把百度 Valet Parking 产品的帧率在数量级上进行了提高。

接下来,我们会陆续发布更多这样的“硬核”技术文章,让更多开发者们更加细致地了解 Apollo 自动驾驶背后的技术。