ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA实时图像CNN卷积计算:从架构设计到落地实践的完整解析

FPGA实时图像CNN卷积计算:从架构设计到落地实践的完整解析 FPGA这块焊了十几年这几年感触最深的是“实时图像CNN”这个组合终于从论文里走出来变成了实实在在的工程需求。不少朋友问我搞CNN加速为什么不用GPU非要在FPGA上折腾这个问题背后其实是整个行业对确定性延迟和功耗边界的追求在变化。今天这篇东西我就把FPGA上实现实时图像CNN卷积计算这件事从架构思路到落地上板完整拆开聊一遍。先说清楚这东西能干什么、适合谁看。如果你手里有个图像采集前端比如OV5640摄像头或者MIPI接口的Sensor想在几百毫秒甚至更短的时间内完成目标检测、缺陷分类或者超分辨率重建并且整机功耗被卡死在几瓦以内那FPGA几乎是唯一解。这篇博文适合有一定FPGA基础、想往AI方向靠的工程师也适合刚接触CNN加速、被各种论文里的名词绕晕的研究生。我会尽量少堆公式多讲硬件视角下的取舍所有内容基于我个人在Xilinx ZYNQ平台上的实际项目经验。1. 整体思路拆解为什么非要用FPGA做CNN1.1 当GPU和CPU都“不划算”的时候很多人一听到CNN第一反应就是上GPU。没错训练阶段GPU是绝对主力这点毫无争议。但推理阶段、尤其是边缘端的实时推理情况就完全不同了。以目标检测为例用一颗中端GPU跑YOLO帧率确实能到几十甚至上百帧但整卡功耗动辄一两百瓦还需要配套的风冷或水冷系统体积和成本都下不来。如果放在工业检测相机、车载前视摄像头、或者无人机挂载设备上这种方案从一开始就不成立。CPU倒是功耗低但算力天花板明显。拿我常用的ARM Cortex-A9双核ZYNQ的PS端来说主频跑到667MHz纯软件方式跑一个简单的3x3卷积层处理720p分辨率的图像单帧延迟能到几十毫秒这还是在没有操作系统开销的理想情况下。一旦图像尺寸变大、网络层数加深实时性就彻底没法看了。FPGA的定位恰好卡在中间可编程的硬件并行度 可控的流水线延迟 瓦级功耗。它不追求极致吞吐量而是用“空间换时间”的思路把卷积计算展开成一张硬件流水网数据从一端进去结果从另一端出来延迟是确定的、可预测的。这种确定性对实时控制类应用来说就是命根子。1.2 功耗与延迟的工程账我们做一个粗略的对比。一颗入门级Artix-7 FPGA比如XC7A35T逻辑资源大概2万个SliceDSP48E1单元90个在200MHz主频下如果专门做3x3卷积并行等效算力可以到几十GOPSGiga Operations Per Second。这个数字听起来不高但对轻量级CNN比如5-8层卷积来说已经够用了而整颗芯片的功耗只有1-2瓦。对应的一颗GTX 1050Ti显卡浮点算力大约2.1TFLOPS但功耗75瓦体积是FPGA的几十倍。在很多嵌入式场景里这76瓦的差距就是“能用”和“不能用”的分界线。还有一个更隐蔽的优势是延迟。GPU处理一帧图像需要CPU把数据拷贝到显存GPU计算完再拷贝回来这个过程引入的延迟通常在几十毫秒数量级。而FPGA可以直接通过MIPI或LVDS接口把Sensor数据流接入计算完的结果再从GPIO或以太网口直接输出整条链路的延迟可以压到1毫秒以内甚至做到逐行扫描、逐行输出结果。1.3 什么样的CNN适合在FPGA上落地不是所有网络都适合往FPGA上搬。我踩过坑之后总结下来适合的CNN通常具备这几个特征层数适中Layer数量在10层以内比较舒服超过20层的深层网络要么循环复用资源导致速度变慢要么资源占用爆炸性价比极低。卷积核尺寸小3x3和1x1卷积是FPGA的“舒适区”5x5以上就需要额外处理资源开销成倍增长。数据量可控输入图像建议在1080p以下超过这个分辨率片上存储和带宽都会成为瓶颈。定点化友好网络对量化精度不敏感或者训练时就已经做了量化感知训练QAT。如果你手里的网络满足上面几条那FPGA加速就是个非常理性和务实的选择。反之如果硬上一个ResNet-50FPGA也不是不能做但投入产出比可能还不如直接上一颗NPU芯片来得划算。2. 卷积计算原理与硬件映射2.1 从数学定义到硬件行为的翻译CNN里最核心的卷积操作数学上就是一个滑动窗口加权求和的过程。对输入特征图的每个位置把卷积核的权重和对应位置的像素值逐项相乘然后累加得到输出特征图的一个点。公式写出来很简单Output[n][y][x] Sum(Input[m][yky][xkx] * Weight[n][m][ky][kx])其中m是输入通道索引n是输出通道索引kx、ky是卷积核坐标。但硬件里没有“循环”这个说法只有空间展开。每个输出通道、每个输出位置的卷积操作本质上就是一堆乘法和加法。如果我们把输入像素和权重提前搬运到计算单元的寄存器里然后用一个周期的乘法器和多级加法树完成累加这就构成了一个最基本的处理单元PEProcessing Element。以3x3卷积、输入和输出都是单通道为例完成一次输出点计算需要9次乘法和8次加法。如果做单通道串行计算在200MHz主频下每秒最多处理约2200万次输出点对于720p1280x720约92万像素图像帧率约24fps——刚好卡在实时边缘但没有任何余量。所以必须并行化。2.2 并行度设计的三种展开方式并行度是FPGA加速的核心杠杆常见的展开方式有三种我分别说一下第一种是输入通道并行。如果输入有3个通道比如RGB那3个通道的卷积是完全独立的可以用3套乘法器同时算最后把结果加起来。这种展开方式对资源消耗最小因为每个通道的权重不同但像素位置是错开的需要行缓冲配合。第二种是输出通道并行。同一份输入特征图要跟不同的卷积核做卷积得到不同的输出通道。每个输出通道的乘法器可以独立计算互不干扰。这种方式能极大提升吞吐量但代价是每一路输出通道都需要完整的输入数据副本对片上存储的带宽要求指数上升。第三种是窗口内并行。把3x3窗口里的9个像素一次性读入同时做9次乘法再用加法树合并。这是最常见的PE内部结构因为9个像素在行缓冲里其实是相邻的读取开销很小。实际工程中这三种方式通常组合使用。我常用的模板是输入通道全并行 窗口内全并行 输出通道部分并行。也就是说3个输入通道各配9个乘法器合成一个输出通道同一时刻计算2-4个输出通道。这样算下来单周期能完成2x3x954次乘加运算200MHz主频下等效算力约10.8GOPS跑一个轻量级CNN的实时推理绰绰有余。2.3 行缓冲图像数据流的心脏CNN卷积最大的特点是局部性——输出某个像素只依赖输入图像中一个很小的邻域。这决定了我们不需要把整帧图像都存在片内只需要在数据流经的时候把当前窗口需要的像素留在手里。这个机制在硬件里叫行缓冲Line Buffer。它的本质是N-1条移位寄存器N是卷积核高度每来一个新像素整体向右移一格最新像素进入第一行最老像素从最后一行移出。这样任何时候行缓冲里都保存着当前像素所在的连续N行数据而FOFiber Optics不是这里我指的是用于窗口采样的9个寄存器窗口寄存器组则始终指向这N行中的某个3x3邻域。行缓冲的深度等于图像宽度。对720p来说每一行需要缓存1280个像素3x3卷积需要2条深1280的缓冲每条缓冲按16bit位宽算共约40Kb存储。这对FPGA内部的BRAM来说毫无压力一个36Kb的BRAM就能装下。但要注意行缓冲的设计必须跟图像时序严格同步每来一个像素的像素有效信号pixel valid缓冲就移位一次不能多也不能少。2.4 权重存储与复用策略权重数据相对固定在网络推理过程中不会变化。通常做法是在系统初始化阶段把训练好的权重通过AXI-Lite或SPI接口写入FPGA的BRAM中作为只读数据使用。但这里有个优化空间如果输出通道并行度是N那么同一时刻需要N个卷积核的权重同时参与计算。如果每个卷积核单独存一份那权重存储开销是N倍。更聪明的做法是把权重按输出通道顺序排布利用FPGA内部的分布式RAMLUTRAM做多端口读出或者把权重展开成适合MAC阵列连续读取的格式。例如将权重重新排列为[输出通道][输入通道][卷积核位置]的三维数组保证同一输入通道、不同输出通道的权重地址连续这样在计算时可以用一个计数器不断递增读取喂给多个PE实现权重复用。3. 关键问题解析实时性与资源约束3.1 实时性怎么定义和度量说“实时”之前先定个标准。我的看法是实时性应该用端到端延迟来度量也就是从图像Sensor输出第一行数据开始到结果数据完整送出的时间差这个时间差应该小于一帧图像的输入周期才叫真正的实时。举个例子。一个720p30的摄像头每帧周期是33.3毫秒有效数据约92万像素按有效传输时间估算实际每像素周期约12.5纳秒80MHz像素时钟。如果我们的流水线能做到“边输入边计算”输入完成后的几个时钟周期内就能输出结果那么端到端延迟就是1帧周期几个微秒的处理时间约33.3毫秒。这时候我们可以说延迟是1帧吞吐率是30fps实时性达标。但有些设计会把整帧存入DDR再处理这样端到端延迟就变成“写入DDR的1帧 从DDR读回处理的1帧 算完写出的1帧”总共3帧时间约100毫秒。这在交互式应用里还能接受但在工业视觉同步控制场景就会出现明显的问题比如高速传送带上的瓶盖检测3帧延迟意味着瓶盖已经跑出去几十厘米了。3.2 资源估算DSP、BRAM、FF的算账方法接到一个新项目我习惯先做一轮资源估算确定FPGA选型再开始写代码。这里分享一个经验公式以3x3卷积为例假设输入通道数Cin输出通道数Cout窗口内并行度W9输出通道并行度PDSP资源约等于 Cin * P 个乘加单元。因为3x3窗口内的9次乘法用逻辑资源或DSP做都行但为了时序更好通常把窗口内乘法也用DSP48E1拆开。如果是专门做乘法一个DSP48E1可以完成一次18x25位乘法而MAC模式下还可以再接一个累加器。实际资源数取决于综合工具的分配策略预估时按每个PE用9个DSP比较稳妥。BRAM资源行缓冲需要的总容量约 Cin * (K-1) * 图像宽度 * 位宽。以Cin3K3宽度1280位宽16bit计算约需要39万bit即约11个36Kb BRAM。激活函数查找表或权重缓存另算。LUT资源加法树、状态机、数据选择器都会消耗LUT。经验上是DSP数量的10-20倍。如果DSP用得多LUT也会水涨船高。拿我的实际案例在Xilinx ZYNQ XC7Z020上实现一个3层卷积网络Cin3Cout从16到32到64输出通道并行度P4输入分辨率720p。最终资源占用大约是DSP 108个292个里占37%BRAM 60块140块里占43%LUT约28000个53200个里占53%FF约31000个。跑200MHz主频综合时序收敛没问题。3.3 定点量化的精度与资源权衡CNN训练时通常用32位浮点但FPGA做浮点运算太奢侈了。一个FP32乘法器在DSP48E1里都装不下更别说加法树了。所以工程上一律转定点最常见的是INT8量化——权重和激活都用8位有符号整数表示乘法器位宽16位8x816累加器位宽32位防止溢出。量化的原理不复杂浮点数乘一个缩放因子并取整映射到[-128,127]区间。关键在于训练时就要做量化感知否则直接后训练量化容易精度掉点。我的经验是对大多数视觉任务分类、检测、分割使用每通道量化per-channel quantization能显著减少掉点程度对FPGA实现的额外成本只是多存几个scale值计算量几乎不变。还有一种更激进的方案是动态定点即在硬件里根据统计特征自动选择小数位宽。这个方案更适合FPGA因为它在保持精度的同时不需要引入复杂的浮点运算单元。具体做法是每层卷积的输出特征有一个统计范围我们用移位寄存器动态调整定点格式使得数据尽量占满整型范围减少舍入误差。这样做的好处是精度比固定INT8高不少代价是每层的缩放参数需要实时计算增加了一点控制逻辑。4. 完整实现流程实录4.1 平台选择与工具链先说选型。如果你只是想学习验证我建议用Xilinx ZYNQ系列原因是PS端ARM和PL端FPGA协同工作方便既可以用PS跑Linux做上位机协议又可以用PL做实时计算。具体型号的话入门选XC7Z010或XC7Z020都够用ARTIX-7的纯FPGA芯片如XC7A35T也可以但少一个ARM核调试时没ZYNQ那么舒服。工具链方面Vivado是绕不开的新版Vitis把HLS和嵌入式开发整合进来了。但是我个人的建议是核心卷积引擎用Verilog/VHDL手写不要用HLS。原因有三个。第一HLS生成的RTL不可控性大时序难收敛第二卷积这种高度规则的数据流手写RTL并不复杂代码量也就几百行第三HLS做浮点仿真很方便但一旦转定点调试困难指数级上升出了问题你根本不知道是C逻辑错还是硬件映射错。4.2 图像采集接口设计图像从Sensor进来首先要完成接口时序的对接。最常见的Sensor输出是MIPI CSI-2或DVP并行数字视频端口。以DVP为例信号包括像素时钟PCLK、行同步HSYNC、场同步VSYNC、数据线D[7:0]、像素有效DE。在FPGA里我们把这些信号打两拍消除亚稳态然后用一个行场计数器跟踪当前像素的坐标。关键是根据DE信号生成像素有效标志pixel valid这个标志是所有下游模块的“心跳”只有pixel valid为高时行缓冲和卷积引擎才工作否则整个流水线处于等待状态。如果用MIPI接口需要在FPGA里实现MIPI CSI-2接收器通常会用Xilinx的MIPI CSI-2 RX Subsystem IP这个IP核能直接把MIPI协议层的数据转换成AXI4-Stream流输出格式是标准视频流AXI4-Stream Video下游处理就方便多了。这块建议直接用官方IP自己写MIPI物理层太容易踩坑。4.3 卷积引擎RTL结构一个完整的卷积引擎我通常划分成这么几个模块第一是cnt_control状态控制模块。这个模块负责生成卷积引擎的工作状态空闲、权重加载、计算、输出等。权重加载阶段通过AXI-Lite接口把权重从PS端写入BRAM计算阶段则根据pixel valid信号驱动整个流水线。第二是line_buffer_pool行缓冲阵列。这个模块内部例化了(Cin x (K-1))个FIFO或移位寄存器用来缓存输入图像的多行数据。设计时要注意当卷积核高度为3时需要2行缓存输入3通道则需要两组2行缓存共6个FIFO。每个FIFO的读端接一个3x3窗口寄存器组。第三是mac_arrayMAC阵列。这是核心运算模块例化了P个计算单元。每个计算单元内部又是一个树形加法器9个乘法器先对9个窗口像素和对应权重做乘法第一级加法树把9个乘积加成一个部分和第二级加法树把Cin个部分和相加最终结果累加到输出通道的累加器。第四是pool_activation池化与激活模块。在某些CNN里卷积层后面会紧跟一个2x2最大池化把特征图尺寸降为原来的一半。这个实现起来很巧只需要比较相邻两行两列的四个输出值取最大值并在正确的时机输出。激活函数方面ReLUmax(0,x)最简单的实现就是判断符号位负数清零即可几乎不消耗额外资源。4.4 一个关键参数计算示例我拿一个非常经典的任务来演示参数计算过程用LeNet-5的改进版做MNIST手写数字识别输入是28x28灰度图第一个卷积层使用5x5卷积核输入1通道输出6通道。先算计算量。输入尺寸28x28输出尺寸24x24每个输出点需要做5x5x125次乘法总共有24x24x63456个输出点所以第一层总共需要86400次乘法。在FPGA上如果主频200MHz假设全部并行展开单时钟周期完成所有乘法这一层耗时约43微秒几乎可以忽略。再看行缓冲。5x5卷积核需要4行缓存每行28个像素每像素8bit共896bit一个BRAM都用不满但硬件还是要分配一个36Kb BRAM的最小单元。然后是资源预估。如果输出全部6个通道并行需要6x25150个乘法器。假设每个乘法器用一个DSP48E1那150个DSP就是XC7A35T能提供的90个DSP的1.67倍明显不够用。所以必须折叠分两轮计算第1轮计算输出通道0-2复用同一套25个乘法器第2轮计算通道3-5。这样DSP消耗75个刚好够。代价是计算时间翻倍第一层总共约86微秒。这个延迟依然微不足道说明轻量级任务在FPGA上绰绰有余。4.5 从理论到上板一次完整的调试记录纸上谈兵半天实际调试才是最考验人的部分。我记得第一次在ZYNQ上跑通这个流程时卡在了一个异常诡异的问题上输入图像是清晰的但输出特征图的上半部分是好的下半部分全部为0。排查过程是这样一个顺序先怀疑是行缓冲的FIFO深度配置错误——如果深度不匹配图像宽度会导致换行时数据错位或丢失。但检查后深度配置正确。接着怀疑是卷积核权重加载出错——把寄存器里的权重值抓出来看初始化的前两行权重正常第三行权重全是0。又检查权重加载的地址递增逻辑发现状态机在计算过程中因为输出通道并行度和输入通道并行度的组合关系地址计数器在计算完第4个输出通道后发生了溢出回卷导致后续权重地址全部错位。这个问题的根源是权重BRAM的地址位宽设置得刚好而地址计算逻辑里用了32位累加器高位的进位没有被截断处理。最后一顿操作——把地址计算逻辑改成模块化计数对最大地址取模并加上地址越界保护问题解决。这类问题文档里往往找不到只能靠仿真和在线逻辑分析仪一步步抓信号。所以调试FPGA-CNN项目我强烈建议从一开始就把ILA集成逻辑分析仪探针挂在关键总线上pixel valid、行缓冲输出、MAC累加器输出、权重地址。等出了板子问题有探针数据和没有探针数据调试效率完全是两个世界。5. 常见问题与工程排雷手册5.1 实时性不达标先查流水线是否被打断很多第一次写卷积加速器的朋友会问为什么仿真时序很好但实测帧率上不去大多数原因是流水线中存在反压back-pressure。比如当输出端FIFO满了下游来不及读走数据时你会不会暂停上游的计算如果暂停了整个流水线的pixel valid就会被拉低造成输入数据断续帧率下降。遇到这种情况最简单的做法是双缓冲输出计算引擎始终把结果写入两个输出FIFO之一当其中一个FIFO的数据正被下游读取时另一个FIFO可以继续接收新的计算结果。这样计算引擎从不等待输出帧率自然就跑满了。5.2 时序收敛失败降低主频还是优化关键路径200MHz的设计综合后时序违例0.3ns这种情况我经常遇到。解决思路有三个方向按优先级排第一检查关键路径的加法树级数。乘法器输出经过多级加法器累加时每一级加法器约增加1-2ns延迟。如果加法树级数太多可以插入流水线寄存器把大组合逻辑拆成2-3个时钟周期完成代价是多一个周期的吞吐延迟但主频可以拉高。第二确认DSP48E1是否被正确推断。有些时候综合器会把乘法器用LUT搭出来时序和资源都吃亏。需要在代码里用乘法运算符*而不是位运算拼接乘法逻辑并在综合属性里设置USE_DSP yes。第三如果上面两步做了还是不行那就接受现实降主频到150MHz。150MHz对大多数720p实时图像处理也够用了省下的时序余量反而让布局布线压力小很多。5.3 资源不够用剪枝与复用如果设计综合后资源爆了先别急着换大芯片有几个优化技巧可以尝试权重剪枝。很多训练好的CNN里接近一半的权重都接近0。如果把这些权重对应的乘法器去掉输出结果几乎没有变化但DSP资源直接减半。在FPGA上做剪枝要稍微小心不是简单地把权重设成0而是要把乘法器从HDL代码里真正删掉权重BRAM也做稀疏化存储。乘法器折叠。如果DSP资源不够但LUT资源有富余逻辑资源通常是DSP的十几倍到几十倍可以用LUT实现一个基于移位相加的乘法器速度稍慢但面积更小。这种方式特别适合位宽较小的数据比如4bit量化。计算引擎时分复用。一个时刻只算一个输出通道把结果暂存在中间缓存下一个时刻再算下一个输出通道。这种方法可以最大限度复用硬件代价是计算时间线性扩大。适用于网络层较浅、但同时处理多路视频流的场景。5.4 精度掉点的调优顺序量化后精度掉得厉害调优的顺序应该是先确认输入数据范围是否合理。图像数据一般归一到0-1但Sensor输出的原始RAW数据可能是10bit、12bit格式直接量化会导致有效位丢失。先做一个预处理把10bit右移2位变成8bit。再检查每层的输出特征范围是否用了正确的统计值。有些后训练量化算法只在全局用了一个scale但不同通道的输出差异可能高达10倍此时必须改用per-channel量化。最后如果精度还是不满意考虑做finetune。在量化仿真环境里对网络做几个epoch的微调让权重适应量化噪声通常能挽回1-2个百分点的精度损失。6. 实用技巧沉淀与个人经验总结聊几个我踩过坑之后沉淀下来的实操技巧未必写在教科书里但非常顶用。技巧一仿真波形里先找pixel valid其他信号都别管。卷积加速器的所有操作都以pixel valid为基准如果这个信号时序不对后面全是白忙。我一般在上板前先用Vivado仿真器跑一段几十微秒的测试激励把pixel valid、行缓冲输出、权重地址、MAC输出四个信号打成一组看波形。如果这四个信号的时间关系对不上基本能定位到具体模块如果对了上板后大概率一次跑通。技巧二用AXI-Stream接口做模块间数据交换千万别自己发明协议。刚开始做加速器的时候我习惯自己定义数据有效信号和握手信号结果模块一多就乱了。后来所有模块之间统一用AXI-Stream协议配合Xilinx的FIFO IP做跨时钟域处理代码清晰度和可维护性立刻上了一个台阶。虽然AXI-Stream的握手逻辑稍微多了几个信号但谁用谁知道。技巧三留20%的BRAM做调试用途。有时候我们需要在运行中抓取某帧的特征图验证中间层的计算是否正确。这需要把特定像素位置的特征图值缓存到一块调试专用BRAM中再通过JTAG或AXI接口读出来。如果BRAM资源一股脑全用了调试时就非常被动。技巧四DSP48E1的累加器可以当第二级加法树用。每个DSP48E1自带一个48位累加器很多人只把它当乘法器用浪费了。我的做法是让乘法器做窗口内的9次乘法中的一次然后把同一输出通道的9路乘法结果依次送入DSP累加器串行累加。这样只需要9个DSP48E1就能完成一个完整输出通道的窗口卷积而不是之前预估的每个通道25个DSP5x5卷积的情况。这个优化能把整体资源下降一半以上非常值得一试。最后再聊点务虚的。做FPGA-CNN加速最容易犯的错是把“算法能力”当成“工程能力”。看了几篇论文、用Python跑通了模型就觉得上板运行也是水到渠成的事但实际上两者之间的鸿沟是定点量化方案的设计、是存储带宽的规划、是流水线的停顿处理、是DSP资源的精打细算。反过来也一样FPGA底层玩得很熟但对CNN的算子和数据流理解不深写出来的加速器也只是个“能跑乘法器”离真正的加速效果十万八千里。所以我的建议是如果你是算法背景就花点时间搞清楚行缓冲和MAC阵列的工作机制如果你是硬件背景就咬牙自己手推一遍卷积的数据流。两边都通了你才会真正理解把一个乘法器织成一张流水网的乐趣远比自己闷头写三百行RTL要过瘾得多。
返回列表