ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA加速CNN卷积计算:从行缓存到乘累加阵列的硬件实现

FPGA加速CNN卷积计算:从行缓存到乘累加阵列的硬件实现 FPGA做CNN说白了就是把卷积神经网络里最耗时的计算部分从GPU或者CPU手里抢过来用硬件逻辑的并行性去硬算。我在这个项目里做的就是在FPGA上实现了一个能对实时视频流做CNN卷积计算的加速器输入是普通的摄像头图像数据输出是经过卷积特征提取后的结果整条链路跑在硬件上不依赖上位机。这个项目适合两类人参考一类是刚接触FPGA但想往AI方向靠的开发者另一类是已经在用GPU做推理、但被功耗和延迟卡住想看看FPGA方案到底能不能打的算法工程师。我会把整个设计思路、卷积计算的硬件拆分、实时性怎么保证、以及我踩过的坑全部摊开来讲尽量让每个环节都能直接抄作业。1. 项目核心设计思路为什么要用FPGA算卷积1.1 卷积计算的计算特征与硬件匹配分析CNN里的卷积层本质上就是一组乘累加运算。一个3x3的卷积核滑过整张图像每个输出像素需要做9次乘法和8次加法再加上偏置和激活函数。假设输入是1080p分辨率30帧每秒第一层卷积是3x3x3的核输出通道64个那一秒钟的运算量大概是1920乘以1080乘以30乘以9乘以3乘以64算下来接近1000亿次乘累加操作。这个量级用CPU做实时基本没戏GPU能做但功耗和体积在嵌入式场景里往往受不了。FPGA的优势在于它的逻辑资源可以按照数据流的形状去定制硬件结构。卷积运算天然是数据并行的——不同输出像素之间没有依赖关系不同输出通道之间也没有依赖关系。GPU是SIMT架构同一时刻执行同一指令遇到分支或者访存冲突就会浪费计算单元FPGA不一样我可以把卷积核拆成一组并行的乘累加流水线再用行缓存把图像数据按滑动窗口的方式喂进去每个时钟周期都能吐出一个有效的输出结果。这里有一个关键认知需要纠正FPGA做CNN并不是去和GPU拼峰值算力而是拼计算效率。GPU的算力很高但数据搬运和调度开销也大FPGA可以为特定的卷积层结构定制数据通路数据从DDR读进来之后在片上SRAM里循环复用尽可能减少对外部存储的访问次数。在我这个项目里片上行缓存的命中率做到90%以上,整体有效算力利用率能拉到60%左右,这在FPGA平台上已经是个相当不错的数字了。1.2 软硬件任务划分哪些计算放在FPGA哪些留在ARM实际的图像CNN处理系统往往不是纯FPGA单芯片就能搞定的。以Xilinx Zynq系列为例芯片上同时有ARM核和FPGA逻辑。我的方案是把CNN推理的卷积层主体全部下沉到PL端也就是FPGA逻辑部分ARM端只负责三件事一是通过DMA把图像数据从DDR搬到FPGA的行缓存里二是配置卷积核权重和偏置参数三是处理CNN最后一层输出的后处理逻辑。任务划分的核心原则是计算密集型的、数据流规整的操作放在FPGA控制逻辑复杂、分支多、数据量小但对灵活性要求高的操作放在ARM。比如卷积、池化、激活函数这些都是FPGA的强项而像非极大值抑制、目标框解码、分类结果的softmax这些在FPGA上写起来很别扭逻辑资源消耗大而且灵活性差留在ARM上跑更合理。我建议在做软硬件划分时先用软件仿真把整个CNN模型的每层耗时跑一遍。比如某个模型一共20层其中卷积层占了95%的耗时那FPGA就把这20层里面计算量最大的15层全部硬件化剩下几层全连接或者特殊的归一化层根据数据量大小决定要不要也放进去。我的经验是如果一层的数据量小于1MB且计算量小于100万次乘加就不值得放在FPGA上搬来搬去的开销比计算本身还大。1.3 技术选型用Verilog手写还是用HLS高层次综合这是FPGA做CNN项目里第一个要做的选择。HLS就是用C/C写算法逻辑然后工具自动转换成RTL代码手写Verilog则是直接从寄存器传输级开始搭。我在这个项目里选择了Verilog手写卷积核心但用HLS工具做了一版原型验证。手写Verilog的好处是可控性强。卷积计算的数据通路每个寄存器在哪一拍打入、每个乘法器和加法器怎么组合我心里都有数。通过Vivado的分析结果我可以精确控制时序和资源利用率。坏处是开发周期长尤其是并行优化的时候控制信号之间的时序关系很容易搞乱。HLS的好处是开发效率高算法工程师写起来没有心理负担一个for循环嵌套就能表达卷积。我在做原型验证时用Vitis HLS写了一个基本的卷积函数加#pragma HLS PIPELINE和UNROLL指令大概一天时间就能跑通功能仿真。但HLS生成的代码在时序优化上经常需要反复调指令和约束而且出现问题的时候不好定位debug起来比手写代码痛苦得多。给个实操建议如果你做的是工业级产品、需要长期维护我建议核心卷积模块手写Verilog控制逻辑和接口部分可以用HLS快速生成如果只是做算法验证和学术研究全用HLS就够了。两条路我都走过最终项目交付时核心计算模块用的是手写VerilogHLS版本作为功能参考留在了仿真环境里。2. 卷积计算的硬件核心实现从行缓存到乘累加阵列2.1 图像数据的片上缓存策略行缓存与滑动窗口CNN卷积计算在硬件上要解决的第一个问题是图像数据的复用。一个3x3卷积核计算输出像素时需要用到输入图像中3行3列共9个像素。如果每次都从DDR里读9个像素数据带宽会被迅速打爆。正确的做法是在FPGA的片上SRAM里维护一个行缓存结构。具体做法是这样的对于3行输入行缓存用三块独立的BRAM块随机存取存储器分别存储第n行、第n1行、第n2行的数据。当计算输出第n行时数据流按列推进每个时钟周期从三块BRAM中各读出一个像素组成一个3x3窗口。窗口在水平方向滑动一个像素后就读下一列。当整个第n行计算完毕三块BRAM同时移位丢掉第n行读入第n3行。整个行缓存的深度只需要等于图像宽度比如1920个像素。如果图像是RGB三通道那需要三组这样的行缓存每组对应一个颜色通道。这样做的好处是DDR带宽的压力被降到了一个很低的水平——每个输入像素只需要从DDR读一次之后的所有复用都在片上进行。我实测下来1080p图像数据从DDR搬运到行缓存的带宽需求只有约200MB/s而DDR3/4的实际可用带宽通常有几个GB/s完全不是瓶颈。滑动窗口的具体生成逻辑里还要考虑边界像素的填充问题。比如卷积核尺寸是3x3在图像最左边一列窗口会越界到图像外面。通常做法是补零或者复制边缘像素。我在设计里加了一个信号padding_mode通过一个多路选择器控制窗口边缘数据源的切换支持零填充和边缘复制两种策略这样既适配了常见的CNN模型也为上层算法留了灵活性。2.2 乘累加阵列的并行度设计输出通道拆分与循环展开有了窗口数据接下来就是卷积计算的核心——乘累加阵列。阵列的规模直接决定了吞吐量。一个设计原则是用FPGA的DSP切片资源来搭建乘累加单元而不是用逻辑查找表实现乘法和加法。Xilinx 7系列的DSP48E1切片一个就能完成18x25位的乘法和48位的累加速度可以跑到几百兆赫兹。我用的芯片是Zynq-7045一共900多个DSP切片理论上可以搭建大量乘累加单元。我的设计采用的是输出通道并行策略。假设输入通道数是3输出通道数是64我一次同时计算4个输出通道。这意味着同一时刻我需要从行缓存中读取3个通道的窗口数据分别送入4组乘累加单元组每组内有9个乘法器和8个加法器用于完成一个输出像素的计算。这样总的DSP用量是4乘以9等于36个每个时钟周期可以产生4个输出像素值。这里有一个关于循环拆分的技巧。传统的卷积实现是遍历输出通道、遍历输入行、遍历输入列、遍历输入通道、遍历卷积核行、遍历卷积核列六重循环。在硬件上我需要把这六重循环中的一部分展开成并行结构。我的经验是把输出通道循环展开把输入通道和卷积核尺寸循环合并成一个串行累加过程。展开哪个维度、串行哪个维度要根据DSP资源和数据复用率来选择。如果并行度太高比如一次计算16个输出通道虽然吞吐量上去了但BRAM的端口数量可能不够用。我的行缓存是三通道同时读BRAM的读写端口是有限的当并行输出通道数超过BRAM端口能提供的数据带宽时就得做数据复制这又会增加资源消耗。所以并行度是一个需要整体权衡的指标不能一味往大了选。我这个项目里4输出通道并行、3输入通道同时读取、9个窗口位置并行计算组合起来是36个乘法器同时工作已经完全满足了1080p30fps的实时要求。2.3 定点数据格式与精度分析卷积计算的乘法累加结果需要存储在特定的数据格式里。GPU上跑CNN多用FP32浮点但在FPGA上浮点计算消耗的资源大约是定点的4到5倍。为了节省资源提高频率我选择了INT16定点格式作为主要的存储格式乘法过程中使用INT16乘以INT8的混合精度。权重我用INT8量化输入特征图也用INT8表示但累加器用INT32。原因很简单两个8位定点数相乘结果是16位多个16位结果累加过程中可能溢出到32位所以累加器至少要32位。最后经过ReLU激活函数之前再从INT32截断回INT16。这个截断可以用算术右移实现我通过一个scale参数来控制右移位数这个scale是在训练后量化阶段标定出来的。很多第一次做FPGA CNN的人会担心定点化以后精度下降太大。我的实际测试结论是在图像分类这种任务上INT8量化后Top-1精度损失通常小于0.5%比如在ImageNet上从72.4%降到72.0%左右在目标检测任务上mAP损失会在1到2个点之间。如果任务对精度特别敏感可以考虑INT16甚至混合精度——但代价是DSP资源翻倍。我踩过一个坑量化时只关注了权重的量化误差忽略了特征图的动态范围。实际上CNN的中间层特征图分布差异很大有的层数值集中在0附近有的层数值跨度很大用同一个scale值可能让某一层严重失真。我的解决方案是在量化阶段逐层统计特征图的数值分布为每一层单独确定scale参数硬件上为每层配置一个scale寄存器在层切换时更新即可。这块工作量说大不大但能明显改善量化后的精度值得认真做。2.4 池化层与激活函数的硬件化CNN里卷积层之后通常跟着池化层和激活函数。池化有最大池化和平均池化两种硬件实现都不复杂。最大池化就是在2x2窗口里取最大值我用4个寄存器暂存一个2x2区域的像素再用比较器树分两级比较得出最大值平均池化则是4个值相加右移2位更简单。激活函数我用的是ReLU判断最高符号位如果是负数就输出0正数就直接透传。整个逻辑只需要一个多路选择器加一个符号位判断一个LUT就能搞定。如果要实现LeakyReLU或者Swish这类更复杂的激活函数在FPGA上就麻烦一些。LeakyReLU需要乘一个小系数可以用一个DSP切片Swish需要计算sigmoid这个在FPGA上一般用查找表加分段线性逼近来做开销会增加不少。我的建议是在设计CNN模型时优先选择ReLU系列激活函数不仅训练简单硬件实现也几乎是零成本。如果必须用复杂激活函数尽量放在CPU上处理只在最后几层用不要在卷积层中间插入太多非线性变换。3. 实时性系统设计与性能优化3.1 实时性指标计算与数据流规划实时性是这个项目的硬指标。我设定的目标是输入1080p30fps的YUV420图像整个CNN处理链路达到30帧每秒的吞吐率。这意味着每一帧图像的处理时间必须小于33毫秒。我设计的数据流是这样的DMA从DDR读取一帧2560x1440的原始图像数据经过格式转换模块把YUV转为RGB这一步用流水线加法器和乘法器实现然后送入卷积计算模块。卷积模块按行处理每次处理3行窗口输出一路特征图。整个过程采用帧级并行流水线——当DMA在搬运第N1帧时卷积模块正在计算第N帧而后处理模块正在处理第N-1帧三级流水同时工作每帧的实际处理时间取决于最慢的一级。卷积模块本身的处理延时是主要瓶颈。即使主频跑到200MHz每个时钟周期输出4个像素一帧1920x1080的图像需要1920乘以1080除以4约52万个时钟周期也就是2.6毫秒。再加上多层卷积串行的累计延时单帧处理时间可以控制在20毫秒以内。33毫秒的预算其实是留了充足余量的。实际规划数据流时要注意一个容易忽略的点卷积层的输出特征图分辨率。很多CNN为了提高精度会在中间层做二分之一或四分之一下采样特征图变小了但通道数变多了。通道数增加对FPGA的处理能力提出了新要求。我的方案是在特征图分辨率较小、单层计算量相对减少的情况下可以通过时分复用机制让同一组乘累加单元分时处理不同的输出通道提高DSP的利用率。3.2 乒乓缓存与DMA搬运的时序协同实时系统里最大的坑是数据断流。一旦DDR数据搬运跟不上卷积模块的消费速度整个流水线就会空转帧率直接跳水。我的解决方法是乒乓缓存。乒乓缓存的意思是把片上的帧缓存分成两个区当一个区在被DMA写入新数据时卷积模块从另一个区读取数据。当DMA写满一个区、卷积模块读完另一个区两个区进行切换。这样避免了读写同一块存储时的仲裁冲突。DMA的搬运和卷积的读操作在时间上是重叠的数据流可以保持连续不间断。DMA搬运本身也要做精心的调度。我的做法是使用Xilinx的AXI DMA IP核配置为Scatter-Gather模式把一帧图像分成多个512字节的描述符每个描述符指定一段连续的物理地址。DMA会连续搬运整个描述符链表搬运完成时产生中断通知ARM核ARM核对下一个描述符链表进行重新配置。这样的好处是DMA连续工作不需要每次搬运都重新启动。实操中的时序协同经验需要让DMA的突发传输长度匹配DDR的页大小。比如DDR3的页大小是8KB突发传输配置为16个256bit的数据正好能凑满一个页。这样读写DDR时的效率最高不会因为频繁换行造成额外的惩罚周期。这个细节我当时是看了Vivado的寄存器读写统计才发现的之前传输效率一直上不去后来改成页大小对齐后带宽直接提升了30%。3.3 时序约束与关键路径优化FPGA设计的最后一步是时序收敛。CNN卷积模块因为乘累加链比较长关键路径往往出现在两个地方一个是行缓存到乘累加阵列的组合逻辑路径另一个是累加器链上多个DSP串行级联的路径。对于行缓存到乘累加的路径我的优化方法是插入寄存器打拍。行缓存输出的数据经过对齐以后先存入寄存器再进入乘法器。这样一来BRAM的输出到DSP的输入就是寄存器到寄存器的路径布线压力小很多。代价是增加了一个时钟周期的延迟但卷积计算是流水线的一个周期的额外延迟完全无所谓。对于累加器链问题在于多个DSP级联时前一个DSP的输出直接作为后一个DSP的输入路径很长。我的做法是用DSP的DSP48E1内部级联功能通过CASCADEIN/CASCAEOUT端口把多个DSP串起来这样累加过程在DSP内部完成不走通用布线资源延迟可以大幅降低。在Vivado里做约束的时候我建议给卷积核心模块单独设置时钟。如果整个系统只有一个200MHz时钟卷积模块、DMA、DDR控制器共用一个时钟域任何一处时序不收敛都会导致整个设计失败。更稳妥的做法是用PLL生成两个时钟一个150MHz给 DDR和DMA使用一个200MHz给卷积核心使用中间通过异步FIFO做跨时钟域数据交换。这样可以分别优化各自的时序收敛的难度会降低不少。3.4 系统级验证从仿真到板级调试验证是整个项目里最磨人的阶段我把它分成三个层次。第一层是功能仿真用Vivado自带的仿真器验证卷积模块的RTL逻辑是否正确。我准备了两种测试激励一种是用C模型生成的随机数据作为输入比对硬件计算结果和软件仿真结果是否一致另一种是用PyTorch跑通一个真实的小模型把每一层的输出导出成二进制文件作为硬件仿真里的参考数据。第二层是逻辑综合加时序仿真。综合完成后看资源利用率和时序报告确认工作频率能达到设计要求。时序仿真是带布线延迟的仿真比功能仿真更接近真实情况。这个阶段最容易发现的问题是复位信号的时序问题——异步复位信号如果有毛刺会导致寄存器进入未知状态系统里检测逻辑经常在这种阶段出错。第三层是板级调试。我一直用ILA集成逻辑分析仪作为主要的调试手段。在关键信号上插入探针比如行缓存的控制状态机、乒乓缓存的切换信号、DMA的搬运完成中断。有一次在板子上发现输出的图像有规律性的条纹用ILA抓取数据后发现是行缓存切换时读地址延迟了一个周期导致行数据错位。这个bug在仿真里不好复现因为仿真用的是理想时钟真实板子的时钟相位噪声会暴露这类问题。板级调试还要注意电源的稳定性。CNN计算全速运行的时候FPGA内核电压的电流需求会突然增大如果电源的纹波过大可能导致DSP单元的计算结果出错。我给核心电压加了几个大容量去耦电容并且用示波器确认了动态压降在50mV以内才敢放心跑全速。4. 常见问题与排查技巧实录4.1 数据路径问题图像花屏、错位、颜色混乱这类问题在FPGA图像处理项目里几乎必然遇到。我经历过的主要有几种现象对应不同的根因问题现象图像显示错位水平方向隔一段就有一条错开的线。大概率是行缓存读地址或DMA搬运的突发长度与图像宽度不匹配。比如图像宽度是1920像素但DMA搬一次突发传输了2048像素多出来的128像素会导致后续行数据错开。解决办法是在DMA描述符里精确配置图像的线性缓冲长度并逐行对齐。问题现象颜色通道错乱红的变蓝、蓝的变绿。这是YUV422或YUV420转RGB时的采样位置没对齐导致的。YUV420的UV分量分辨率只有Y的一半UV采样位置错一个像素就会导致整个画面颜色偏掉。我写了一个专门的测试图样比如纯红、纯绿、纯蓝的色块逐块确认转换结果。问题现象图像闪烁或偶发花屏。这类随机性问题先查DMA的地址连续性和DDR的访问冲突。我在DMA中断处理里加入了帧计数和地址回读确认每一帧的搬运源地址和预期一致。另一个常见原因是DDR控制器的刷新周期和DMA的实时带宽要求冲突调整DDR控制器的仲裁策略可以缓解。4.2 时序收敛问题关键路径超时和布局布线的博弈Vivado的时序报告经常让人头大。报红色的路径可能长达几十纳秒远超时钟周期。我的经验是先看报告里是哪一段路径再决定处理方式。如果关键路径是LUT到LUT的组合逻辑通常是因为逻辑层级太深。比如计算窗口均值时累加器链超过了5级就要考虑改用DSP切片或者重新做运算树的平衡。乘累加阵列的求和树我一般用4输入加法器构成平衡树而非简单的串行加法这样逻辑深度从9级降到3级。如果是DSP到BRAM的路径超时可能是数据位宽太大导致布线延迟增加。比如累加结果是32位但BRAM输入只需要16位就应该在数据写入BRAM之前做截断寄存避免大位宽信号走太远。另外值得一提的是Vivado的物理综合选项比如phys_opt_design的INSERT_SLL选项有时候效果奇好用一下可能直接让时序过线。4.3 资源冲突问题BRAM和DSP的合理分配FPGA里的BRAM和DSP数量有限设计过程中经常出现资源不够或者利用率极低的情况。我的建议是先用综合报告看各个模块的资源消耗找出最吃资源的模块。BRAM资源不够的常见原因是缓存图像数据的行缓存过深。比如4K分辨率的行缓存每行3840像素16bit宽度三个通道再加双缓冲需要的内存就非常可观。如果芯片BRAM不够用可以把行缓存改小用分块处理的方法比如把图像分成上下两个半帧分别卷积再拼接结果。DSP资源不够的时候优先检查有没有在不需要的地方用了乘法器。比如计算卷积结果使用了乘法器的同时如果某个控制信号的计算也用了乘法器就会出现浪费。控制信号的乘法可以改用移位加法的形式节省DSP给真正的卷积计算。4.4 实现精度和实测性能的对账方法最后养成一个习惯每个版本都能对照实测结果和理论预测的差距。如果实测帧率比理论值低很多先从数据断流找原因如果实测功耗比预测值高很多先从数据翻转率找原因。我在完成当前版本后对着Vivado的报告把整体项目的资源利用率、DSP利用率、BRAM利用率、时钟频率、整帧处理时延记录在一张表里方便后期做性能基准和版本对比。5. 项目优化过程和后续演进方向5.1 第一版到第三版的性能迭代记录这个项目我一共经历了三版大的迭代。第一版用HLS快速原型主频能跑到150MHz处理1080p30fps勉强够但资源利用率很高DSP几乎占满没有留下任何余量。第二版换成手写Verilog重构核心模块主频提升到180MHzDSP利用率降到60%并且加入了乒乓缓存和DMA分块搬运数据流顺畅了很多。第三版进一步优化了累加器树的逻辑深度和BRAM的使用策略主频稳定在200MHz整帧处理时间控制在15毫秒以内已经完全满足实时性要求。每一次迭代我都会更新一个性能对比表把主频、资源、帧率、功耗的记录保存下来。这样后期如果要换芯片做国产化替代也可以直接参考这些数据。5.2 从单模型到多模型模型切换与动态配置项目后期的一个新需求是同一个硬件平台支持多个CNN模型切换。比如一个模型做图像分类一个模型做物体检测系统根据应用场景动态加载对应模型。硬件上卷积模块本身是通用的切换模型只需要更新权重存储器的内容。我在DDR里划分了一个模型区ARM启动时把模型文件解析成权重二进制通过DMA写到FPGA的权重BRAM中。整个模型切换时间控制在200毫秒以内不影响业务逻辑的实时性。权重存储是个容易被忽略的细节。一个典型的CNN模型5层卷积的权重可能有几十MBFPGA片上BRAM放不下只能放在DDR。我的做法是把权重也按行缓存的思路组织卷积计算到某一层时才把该层权重从DDR预取到片上权重缓存中。这样既保证了计算连续性又不浪费BRAM资源。5.3 关于后续可以尝试的新方向如果继续往深处做我建议关注这几个方向一是模型压缩配合硬件设计比如对权重做剪枝后硬件的计算和存储都能进一步节省二是尝试更先进的封装架构比如把多个FPGA组成集群做更大模型的并行推理三是在FPGA上集成更多AI算子比如目前CNN中常见的卷积层之外的注意力机制和Transformer模块。FPGA的优势在于灵活自定义这些新方向的硬件化实现都是可以持续深挖的课题。说句实在话FPGA做CNN并不轻松比起调包调参的软件玩法它的每个细节都需要硬件工程师亲自打磨。但当你看到摄像头拍进来的画面经过自己写的硬件管线实时输出识别结果的那一刻那种成就感是纯粹的软件实现给不了的。如果这个项目让你对FPGA加速深度学习产生了兴趣建议先从一个最简单的单层卷积开始把行缓存和乘累加阵列调通再逐步加层、加优化这条路走下来收获会非常大。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表