深入理解FPGA加速原理——不是随便写个C代码去HLS一下就能加速的

不要写代码做无意义的操作

写这篇文章的起因是前段时间有人在网上问了我一个HLS的问题,他写的C代码HLS时报错,问我是咋回事。可惜的是由于我水平低,没玩过HLS,所以我也不知道是咋回事。不过我扫了一眼他的代码,再加上他的介绍,我看出来他的代码是干了下面这样一件事情:

深入理解FPGA加速原理——不是随便写个C代码去HLS一下就能加速的_第1张图片

他的FPGA的DDR里存了一副较宽的图像,这副图像是三张图片拼接起来从HDMI接口传进来存到DDR里的。FPGA要做的事情大概是要再把这3幅图像拆开分别做些仿射变换之后再拼成一副尺寸不同的大图像。他写的要HLS的代码所做的事情就是把存在一起的拼接图像从DDR中读出来再分开存入DDR中,他说这是为了下一步好做双缓存。

我了解到这个就觉得这个把图像从DDR里读出来再换个地址存进去,然后啥计算都没有进行,这个操作完全没有任何意义啊!然后我就跟他说,你干嘛要这样弄,你要想把图像分开来存,你把从HDMI存进DDR那个地方的代码改一下,在那时直接分开存不就可以了。但他可能没写过Verilog代码,不知道还可以这样改,所以我说的话估计他也没听懂。

要知道上述这种把数据从DDR中读出来啥都没干又换个地址存进去的操作是完全没有意义的。要达到这个目的在最初存进DDR的时候每到1/3和2/3行的时候改一下地址就行了。把数据从DDR里读出来又写进去,完全是在浪费DDR的带宽,浪费功耗。最关键的是浪费了时间。假如这个项目是要做VR显示的加速,VR不就是要尽可能的低延时么?就算DDR带宽足够你浪费,功耗你也不在乎,但这一读一写徒增的很多延时你也不思忖一下?

要想加速得先学一下并行计算原理

所以不论你是想用FPGA还是GPU做加速,都得先学一下加速的原理,和并行计算的编程技巧。不是你随便写个代码扔到GPU编译器编译一下,或是HLS一下扔到FPGA里就能加速。网上讲FPGA加速原理的资料不多,但讲GPU编程的教程有很多。我看过B站上的《nVIDIA CUDA 高度并行处理器编程课程》的这部视频:

https://www.bilibili.com/video/BV1tC4y1H7QG?from=search&seid=10701032254566098632

GPU的瓶颈之一在于运算单元喂不饱

看完这个视频我们就会知道GPU里有很多并行运算单元,只有当这些运算单元一直都在做运算的时候才能发挥出最大效率。或许你会问,难道GPU里的运算单元还会有闲着的时候?有的,当你的代码写的不好,没做多少运算就要从DDR里取数据的时候,运算单元就有可能会因为取数据的延时而在那里等待。GPU能统计出运算单元的使用效率,如果你的代码在GPU上跑的时候运算单元的利用率只有百分之二三十,那就是比较低了。所以GPU要提高效率,也是要充分利用片上内存,减少DDR的访问频率,这和CPU要优化Cache命中率是一样的。而这个事情GPU的编译器会做一部分,但也做不到完全智能,所以还是需要编程者要有一定的技巧,不是任何代码拿去就能效率高的。不过好在现在做机器学习用GPU加速都有别人写好的框架,里面GPU的代码都已经有高手写好了,不用我们去操心这个GPU代码该怎么写的问题。

FPGA加速的优势到底在哪里?

FPGA的DDR带宽比不上GPU,运行的频率也比不上GPU,里面的运算单数量元之前好像也比不上GPU,那FPGA的优势到底在哪里?

https://www.aldec.com/en/company/blog/167--fpgas-vs-gpus-for-machine-learning-applications-which-one-is-better

中文的比较FPGA和GPU的文章都似乎没把这个问题讲的太明白,这篇英文的讲的还不错,谷歌翻译一下就能看,文章一开始是这样说的:

Raw Compute Power: Xilinx research shows that the Tesla P40 (40 INT8 TOP/s) with Ultrascale+TM XCVU13P FPGA (38.3 INT8 TOP/s) has almost the same compute power. When it comes to on-chip memory, which is essential to reduce the latency in deep learning applications, FPGAs result in significantly higher computer capability. The high amount of on-chip cache memory reduces the memory bottlenecks associated with external memory access as well as the power and costs of a high memory bandwidth solution.

意思就是Tesla P40和那款FPGA在原始算力上是差不多的。但对于片上内存这一项,FPGA则有着显著更高的计算容量,而片上内存在深度学习等应用中对于减少延时是至关重要的。大量的片上cache缓存减少了外部内存读取带来的内存瓶颈,也减少了高内存带宽解决方案所需要的功耗和成本。

要知道访问外部存储,比如读写DDR,是非常耗能的,可能数据在DDR和芯片之间跑来跑去的能耗比芯片本身做计算的能耗还要高。FPGA片上内存的大容量和灵活配置能力能减少对外部DDR的读写,这自然能缓解内存瓶颈,也就是减少延时并降低功耗。那FPGA具体是如何做到这点的呢?

FPGA最擅长实时流水线运算

啥叫实时呢?实时的意思就是我的运算速度和数据来的速度是一样的,不比它快,也不比它慢。快了也没用,数据都没来你也算不了。慢了就不能叫实时了。比如1920×1080×60帧的视频流,像素时钟频率约是150MHz,FPGA在对它进行处理的时候运算时钟频率就是150MHz,用的就是像素时钟。一个时钟周期来一个像素处理一个像素,来一行处理一行。

流水线的意思是,第一阶段的处理和第二阶段的处理不是第一全部完成了再第二,而是大部分重叠在一起的:

深入理解FPGA加速原理——不是随便写个C代码去HLS一下就能加速的_第2张图片

如上图所示,CPU做图像处理一般要等到一帧图像传到电脑内存之后才开始,如果要算两次卷积,那就只能先算第一个,再算第二个。

FPGA做运算就不是这样。首先FPGA能在图像一边从HDMI或传感器芯片传过来的时候一边就开始做处理。而CPU一般是等一帧图像传完了之后才开始处理的(嵌入式处理器能不能做到一边传一边处理我也不知道)。比如说每秒60帧的图像从传感器传出来一帧要1/60秒时间,CPU只能等着1/60秒完了之后才开始处理,而FPGA则是可以从图像开始传的0时刻开始,等图像1/60秒传完时也几乎处理完了,能达到最高的实时性。

如上图所示,CPU做一次卷积的时间是要小于FPGA的,但FPGA能充分利用图像正在传输的时间,还能并行流水线的进行第二次卷积,所以FPGA运算的延时最小,能达到最高的实时性。当然条件是把两次卷积并行起来需要两倍的运算单元,所以如果能够不惜代价的增加运算单元,也就是裸堆FPGA芯片,是可以把大量计算全都并行起来达到实时速度的。

最高效的计算方式应该是怎样的?

现在大家应该都知道了,计算的瓶颈往往不在于运算单元数量的不够或是速度不够高,而是在内存带宽上,也就是数据读写来不及,运算单元喂不饱。那最高效的计算方式自然就是能充分利用片上高速内存,让运算单元等待数据的情况从不发生,同时尽可能的减少了对外部DDR的读写,这也同时降低了功耗,所以既要速度快又要功耗低这两者其实并不矛盾

为什么FPGA比GPU的能耗更低呢?上面讲的是原因之一,还有一个原因是FPGA是硬件可编程,所以它的数据通路是最直接的。比如说用FPGA算两个数相加,那直接把两个数从内存读进来送到加法器那去加就行了。但在GPU中还要进行指令译码,知道是做加法之后还要把这两个数往加法器那里送,这中间数据要经过一些选通器才会被正确的送到加法器那而不是别的运算单元那里。译码,数据经过选通,这些都会产生额外的功耗。而FPGA每次要做的运算都是固定的,所以不需要指令译码,也基本不需要数据的选通。

最高效的方式往往只有一种,低效的方式有无数种

两点之间直线最短,所以从A到B除非你会瞬间穿越,否则就是走直线这一条路径是最短的。而如果你想绕弯则可以有无数种绕法。同样的道理,进行一种运算的最高效方法在某种运算平台或框架下往往也只有一种。就拿3x3的图像卷积为例,怎样算才最高效呢?

3×3的卷积每次需要同时取三行图像中的三个像素值,也就是做第1行的卷积时需要取第0,1,2行的数据,第二行时需要取1,2,3行,再是2,3,4行。算了三行数据,就要读取9次行数据,那么在最低效的情况下,一副图片卷积完,图像数据实际上是要被读取三遍的。那要是5x5的卷积就是要读取5遍了。这个读内存的操作就太多了,自然会成为瓶颈。

那最高效的方法是什么呢?自然是只需要读取一遍图像就把不管3x3还是5x5的卷积都算完。实现的方法就是用片上内存把前h-1(h为算子高度)行的图像数据都缓存着,这样第h行数据来的时候就可以直接从片上内存中把前几行的数据读出来,不用都从外部DDR中读取。而能完美实现这种运算操作的就只有FPGA了,CPU Cache小,肯定是不行的。GPU行不行我不清楚,就算计算一层卷积GPU效率也可以很高,那要接着再算一层呢?

再回到上面那幅图,CPU算两次卷积是先读一次图像,算出结果存回去,算完一遍后再把刚才存的结果读出来进行第二遍计算。也就是说CPU在进行连续图像处理的时候必须把中间结果存回DDR去再读出来。GPU估计也得这样,但FPGA如果片上内存足则可以不这样。FPGA可以把第一层卷积的结果也缓存在片上内存中,然后流水线式的进行第二层卷积。以3×3的卷积为例,当第一层算到出第三行结果的时候,就可以和之前缓存的两行结果开始进行第二层卷积了。那么第二层卷积的第一行结果就能在原始数据读到第五行的时候出来,如下图所示:

深入理解FPGA加速原理——不是随便写个C代码去HLS一下就能加速的_第3张图片

所以说如果FPGA片上内存和运算单元够足,数量也管够,它是可以实现图像只传输或读取一遍,就把好几层的卷积都算出来,中间结果和图像都无需再存到DDR中然后再读出来。既能实现最高的实时性,(16层3x3卷积只需延时32行,才十几分之一帧的时间),也能实现最小的能耗。

卷积代码有现成的,无需HLS

都说FPGA编程难,所以现在有HLS,但其实FPGA如果只做数据流运算,代码写起来就没有那么多,也没有那么复杂。数据流运算的代码比较单纯,没有写状态机,写CPU那么难写。而且数据流运算的模式往往也是简单重复的,比如要做多个卷积核的很多层卷积,那这不都是卷积么?写一个卷积代码出来然后复制粘贴,像搭积木一样把它们搭起来不就行了?需要写很多代码么?需要HLS么?

https://github.com/becomequantum/Kryon

这个库“FPGA Image Process.v”文件夹中的GrayOperator3x3.v就是一个对灰度图像进行3×3卷积的实例代码。代码中的ArrayXX就是3×3的图像数据阵列,给这个模块增加一组或多组权重输入,让各组权重乘以对应的ArrayXX再把结果加起来就完成若干组卷积了。卷积的结果可以输出到别处,也可以再复制一个这个模块,把结果当作PixelData输入给这个模块,就能进行下一层的卷积了。就这么简单。

你可能感兴趣的:(人工智能)