ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA实现LeNet推理加速器:从RTL设计到定点量化全流程解析

FPGA实现LeNet推理加速器:从RTL设计到定点量化全流程解析 1. 项目概述与整体规划方向1.1 为什么偏偏是LeNet近段时间一直在梳理CNN硬件加速器的设计方法论第一个完整跑通的项目选了LeNet。原因很简单LeNet的网络结构足够经典但又不至于过分简化到失去代表性。它包含卷积层、池化层、全连接层、非线性激活函数这些CNN的基本组成单元参数总量约6万单张28x28的MNIST图像推理所需的乘累加操作在几十万量级这个规模用FPGA做纯逻辑实现既不会因为资源不够而捉襟见肘也不会因为规模太小而失去实际工程参考价值。另外一个更实际的原因是LeNet是绝大多数人接触深度学习的启蒙模型大家对它的结构、参数、输出维度都烂熟于心。这意味着做硬件加速器时软件参考模型可以直接手写Python实现不需要依赖任何深度学习框架对结果也容易检查。硬件加速器设计最怕的就是软件参考模型不可控一旦计算结果对不上你根本说不清是RTL的bug还是参考模型的bug。LeNet这种结构透明、规模可控的网络是最理想的硬件设计练手对象。这个项目的目标很明确在FPGA上实现一个LeNet的推理加速器输入一张28x28的灰度图像输出10个类别的置信度分数。不涉及训练只做推理。板卡选用Xilinx Artix-7系列开发环境用VivadoRTL语言用Verilog。后续如果有机会再换ZYNQ做PS-PL联合验证但第一阶段先把纯PL的推理通路跑通。1.2 硬件加速器到底是什么思路很多人第一次接触硬件加速器会有一个误区以为要用硬件把整个神经网络复刻一遍。实际上硬件资源是有限的FPGA上乘法器、BRAM、逻辑单元都有上限不可能每个卷积核都独立分配一套并行计算单元。真正的加速器设计核心是——在有限的硬件资源下通过时分复用和空间并行的组合把计算过程有序地调度到硬件上。打个比方软件跑推理就像一个人在做菜按顺序一道一道来硬件加速器则像一个设计好的中央厨房有固定的灶台数量乘法器阵列有固定的备菜区片上缓存有固定的传菜通道总线你要做的是设计一套合理的流程让几十道菜在这几个灶台上高效轮转尽可能减少灶台空闲时间。LeNet的推理过程可以分解为五个阶段卷积层1CONV1、池化层1POOL1、卷积层2CONV2、池化层2POOL2、全连接层FC。每个阶段的计算模式和访存特征都不一样。卷积层是典型的滑动窗口计算计算密度高池化层几乎没有乘加运算主要是比较和选择逻辑数据搬运为主全连接层则是矩阵向量乘权重占比大计算密集但数据复用性低。设计的时候必须分层对待不能一套架构通吃到底。这个项目我给自己定的设计原则有三条第一控制模块的复用率卷积计算单元可以通过参数配置复用于全连接层池化单元和激活函数单元可以在不同阶段切换使能第二数据流用AXI-Stream风格的自定义握手协议统一便于后续挂接DMA或者PS端第三所有中间结果必须落回BRAM不占用额外寄存器资源。这三条原则贯穿了整个设计过程越到后期越觉得当初定得及时。2. 核心设计思路与关键参数计算2.1 LeNet参数摸底与计算量测算动手写RTL之前花了两天时间把LeNet的每一层参数量和计算量算清楚。这一步绝对值得后面所有硬件资源评估、缓存深度设计、时序预算都依赖这张表。LeNet-5的原始结构输入是32x32的图像MNIST原始是28x28有些实现会先padding到32x32后面的实现也常直接按28x28处理具体参数如下层名输入尺寸卷积核/权重输出尺寸参数量乘加操作数CONV11x28x286个5x5卷积核6x24x241x6x5x51506x24x24x2586,400POOL16x24x242x2平均/最大池化6x12x1200CONV26x12x1216个5x5卷积核16x8x816x6x5x52,40016x8x8x(6x25)245,760POOL216x8x82x2池化16x4x400FC316x4x4256256x120权重120256x12030,720120x25630,720FC4120120x84权重84120x8410,08084x12010,080FC58484x10权重1084x1084010x84840汇总一下总参数量约60,930个权重总的乘累加操作数约37万次不计算池化层因为池化不涉及乘法。这个规模对FPGA来说属于轻量级但对架构设计来说五脏俱全非常适合用来验证加速器设计方法。2.2 位宽选择与定点量化方案CNN硬件加速器绕不开的一个问题就是数值表示。浮点运算在FPGA上可以用DSP48的浮点IP实现但资源消耗大、频率做不高、逻辑非常复杂实时性要求高的场景一般不这么干。工程上最常用的是定点数方案把浮点权重和激活值统一量化成定点数用整数乘加替代浮点乘加。这个项目里我选的是8bit有符号定点数权重和激活都用Q7.0格式1bit符号位7bit整数部分偏置和中间累加结果用32bit定点数。为什么选8bit而不是16bit因为LeNet这种浅层网络对量化误差的容忍度比较高8bit定点在很多研究里已经被证明在MNIST这类简单任务上几乎无损。而且8bit定点乘法在Artix-7上正好可以用一个DSP48E1实现如果切16bit就需要两个DSP拼一个乘法资源消耗翻倍。量化过程分三步做第一步统计权重范围。用训练好的LeNet模型导出所有权重统计最大绝对值和分布。我的模型权重大部分集中在-0.1到0.1之间最大值约0.258bit有符号整数范围是-128到127直接用Q7.0格式表示小数精度明显不够。常规做法是引入缩放因子即把浮点权重乘以一个系数映射到整数范围。这里我选了全局共享的缩放因子方案对所有层的权重统一乘以256后四舍五入取整。这样在硬件端只需要在最终输出时做一次右移8位的缩放无需逐层维护不同的缩放因子。第二步验证定点化后的精度。用Python写了一个纯整型运算的LeNet推理脚本把浮点权重加载进来量化成int8然后模拟硬件里的定点乘加流程中间累加用int32对比浮点模型的输出和定点模型的输出。实测在MNIST测试集上定点模型的准确率比浮点模型低约0.1%以内最坏情况单样本输出置信度偏差在2%以内完全可以接受。第三步确定激活函数的定点实现。LeNet原始用的是tanh激活但现在主流实现都换成了ReLU。ReLU在硬件里实现成本极低——判断最高位符号位如果是0就直通如果是1就输出0不消耗任何DSP资源。我最终选了ReLU一方面硬件友好另一方面精度和收敛性也不会比tanh差。2.3 片上缓存容量测算与BRAM规划CNN推理过程中中间结果如果全部存到DDR里访存延迟会严重拖慢推理速度。所以片上缓存的设计是加速器性能的关键。LeNet各层中间结果所需存储量如下CONV1输入1x28x28x8bit 6272bit ≈ 0.75KBCONV1输出也是POOL1输入6x24x24x8bit 27000bit ≈ 3.3KBPOOL1输出也是CONV2输入6x12x12x8bit 6912bit ≈ 0.82KBCONV2输出也是POOL2输入16x8x8x8bit 8192bit ≈ 1KBPOOL2输出也是FC3输入16x4x4x8bit 2048bit 0.25KBFC3/FC4中间结果120x32bit 84x32bit ≈ 0.8KB用32bit累加器权重存储60930x8bit ≈ 59.5KBArtix-7系列我手头这块板子是XC7A35T有50个36Kb BRAM每个36Kb BRAM可配置为36Kb x 1bit或18Kb x 2bit、9Kb x 4bit等不同位宽组合总计约1.8Mb的片上存储。按位宽8bit换算大约能存225KB数据。60KB的权重加上各层中间结果全部塞进BRAM完全没有压力。所以这个设计不需要接DDR也不需要复杂的DMA调度所有数据都可以片上搞定整体架构会简单很多。BRAM规划是这样的权重存储用4块BRAM并联组成64bit位宽的只读端口分时读出各层权重输入图像和中间特征图用双端口BRAM一端口写一端口读支持流水线化的边算边写全连接层的偏置和中间结果单独用一块小容量BRAM存放。这个规划在后期实现时做了一些微调但总体方向没变。2.4 并行度选择与复用策略确定了数据表示和存储方案接下来最关键的问题是同一时刻安排多少个乘法器并行工作Artix-7 XC7A35T上有90个DSP48E1。每个DSP48E1可以配置为一个独立的8bit x 8bit乘法器结果16bit。理论上我最多可以例化90个并行乘法器做全并行计算。但这显然不现实因为BRAM读写端口数量是有限的喂数据的速率会成为瓶颈。我最终选择的是16个并行乘累加单元PE 时分复用的方案。具体来说每个PE内部包含一个乘法器、一个累加器和一个寄存器组。16个PE并行工作主频100MHz理论上每秒可以完成1.6G次乘加操作。37万次乘加在理想无停顿情况下只需约0.23ms即可完成留有很大裕量。这个16并行度是怎么确定的核心是平衡DSP资源和数据加载带宽。CONV1需要6个输出通道每个输出通道对应25次乘累加6个通道正好6 x 25 150次乘累加是16的约9.4倍。CONV2需要16个输出通道每个输出通道对应6 x 25 150次乘累加。如果16个PE全部用于输出通道并行则每个PE负责一个输出通道的完整计算输入数据从单端口读广播给所有PE这在数据带宽上是最优的。所以我把并行维度定为输出通道并行而不是输入通道并行或空间位置并行。有人可能会问为什么不用更多PE来加速因为输出通道并行时每个PE内部是串行累加输入通道和卷积窗口的16个PE对应16个输出通道同时计算。CONV2的16个输出通道刚好对应16个PE一次卷积窗口计算就同时完成16个通道的MAC累加。如果用8个PECONV2的16个输出通道需要分两轮处理计算时间直接翻倍如果用到32个PECONV2时又会有16个PE闲置。16这个数字恰好是CONV2输出通道数整层计算不需要切分控制逻辑最简单。3. 硬件架构设计与RTL实现3.1 顶层模块划分与数据通路整个加速器的顶层架构按照功能划分为六个模块控制模块Ctrl、输入缓冲模块InBuffer、权重存储模块WeightROM、计算阵列模块PE Array、激活/池化模块ActPool、输出缓冲模块OutBuffer。外加一个全局复位和时钟管理单元。数据通路是这样的外部通过AXI4-Lite接口把图像数据写入输入缓冲BRAM然后Ctrl模块产生一组起始信号PE Array从InBuffer和WeightROM读取数据完成指定层的卷积/全连接计算中间结果按顺序写回InBuffer复用存储空间经过ActPool模块的激活和池化处理后进入下一层。最终结果放在OutBuffer中主机通过AXI4-Lite读取。这里有一个设计细节值得说所有中间层的数据都复用同一块BRAM空间不单独开新存储。因为CNN是串行推理CONV1的输出在进入POOL1后就不会再被读取所以它的存储空间可以被CONV2的输出覆盖。这个空间复用策略在具体实现时通过一个地址重映射逻辑完成Ctrl模块根据当前层号生成不同的基地址偏移。这样做的好处是大幅节省BRAM坏处是地址计算逻辑稍微复杂一点调试的时候要格外小心非常容易因为地址覆盖把正在用的数据冲掉。3.2 卷积计算单元的具体实现卷积层的计算单元是整个加速器的核心。LeNet的卷积核是5x5我把PE内部的输入数据组织方式设计成行缓存line buffer模式按行读入输入特征图每读5行数据就形成一个5x5的窗口与权重做乘累加。具体到RTL实现每个PE内部有一个25深度的权重寄存器组25个5x5权重的8bit值和一个25深度的输入数据寄存器组。当收到窗口有效信号时25个乘法器如果用DSP资源那需要25个DSP同时计算25个乘积再用加法树累加。但这样每个PE就要25个DSP16个PE就是400个DSP远超器件容量。所以实际实现时做了折中每个PE内部只有一个乘法器25个乘累加操作串行完成。这样每个PE消耗1个DSP16个PE共16个DSP加上其他逻辑总DSP用量大约30个在90个预算内留足了余量。代价是卷积一层的时间会拉长但如前所述37万次操作在16个PE并行下仍然很快。行缓存模式的实现是另一个细节。输入特征图按行写入时我维护一个行号计数器每当计数到5的倍数时说明已凑齐5行数据可以开始拼接5x5窗口。窗口中每一行的5个像素用移位寄存器实现每来一个像素时钟移位寄存器右移一位存满5个后输出。5行窗口数据并行读出后逐次送入乘法器与权重相乘。这里有个容易踩的坑5x5卷积核在图像边界时窗口会越界。我选择的是不对输入做padding所以CONV1输出尺寸是24x24而不是28x28与原始LeNet一致省掉了padding逻辑。后期如果要支持其他网络结构再在输入缓冲前加padding模块不迟。3.3 池化层与激活模块设计池化层我用的最大池化max pooling窗口2x2步长2。在硬件上实现最大池化非常简单当一行中的两个像素都有效时比较器选出较大值然后与下一行对应位置的两个像素再比较最终输出2x2窗口的最大值。时序上最大池化可以直接跟卷积输出做流水线。当卷积的输出特征图按行写入时ActPool模块只需要等待每两行数据写完后再按列成对读出做比较。我设计的是边写边池的模式写地址计数到偶数行、偶数列时触发一次池化比较输出结果直接写入下一层缓存。这样整个池化过程不需要额外的存储中转也几乎没有额外周期开销。激活函数ReLU放在卷积累加之后、池化之前。8bit定点数在累加过程中会扩展到32bitReLU判断的是累加结果的最高位。符号位为0说明是正数截取高8位后输出符号位为1则输出0。这在一拍内就能完成不消耗额外资源。虽然ReLU实现很简单但有一个细节值得注意如果不对激活输出做饱和截断负数取反后直接截位会导致数据溢出。比如一个16bit的负数取符号位判断为1后直接置0没问题但如果是正数累加结果超过了8bit能表示的范围-128~127直接截取高8位会得到错误结果。所以ActPool模块里必须加一个饱和处理逻辑如果累加结果大于127输出饱和到127小于-128输出饱和到-128。这个逻辑在实际验证时确实发现过问题最初没有加饱和导致个别像素点数值出错排查了很久才发现是溢出问题。3.4 全连接层如何复用卷积硬件全连接层本质上也是乘累加所以没必要单独设计一套硬件。我的做法是把全连接层当作1x1卷积来复用PE阵列只是数据加载方式不同。以FC3为例输入是POOL2输出的256个特征值16通道x4x4需要计算 120 个输出神经元。每个输出神经元对应256次乘累加256个输入与256个权重相乘再累加。我安排16个PE并行计算16个输出神经元每个PE内部串行累加256次。完成16个输出后切换权重继续下一批16个输出直到120个全部计算完。权重存储的排列方式对全连接层的访存效率影响很大。软件里全连接权重是按 [输出神经元数 x 输入维数] 排列的即一行对应一个输出神经元的所有输入权重。硬件里我把它按 [输入维数 x 输出神经元数] 重排这样同一时刻16个PE需要的16个权重地址是连续的可以一次性从BRAM读出一段16个字节喂给16个PE避免了碎片化读取。在RTL实现时权重ROM的读地址总线是共享的16个PE共用一组地址但从权重ROM读回的数据按顺序分配给各个PE。这个地址广播数据分发的模式说起来简单实际写代码时最容易出错的是16个PE的累加时序必须完全同步任何一个PE因为等待数据而插入气泡都会导致整个流水线的握手错位。所以我在全连接层的状态机里专门加了一个PE完成同步屏障只有16个PE全部累加完成才统一更新下一组输入和权重。4. 存储设计、数据流调度与协议细节4.1 权重存储与排列方式权重存储是整个设计里最琐碎的部分。60930个权重如果直接按全连接顺序排卷积层读取的时候会很痛苦。我的做法是按层划分存储区域每一层内部再按输出通道优先排列。以CONV1为例6个输出通道每个通道25个权重。存储排列是通道0的25个权重紧挨在一起通道1的25个权重紧挨在一起以此类推。计算某个输出像素时依次读出该输出通道的25个权重与输入的5x5窗口对应元素做乘累加。CONV2因为是16个输出通道、每个通道6x25个权重排列方式同理只是每个通道的权重块更大。这里有个存储效率的问题BRAM的最小读写粒度是字节8bit权重ROM如果按单字节读25个权重需要25个读周期效率太低。我的做法是把4个相邻输出通道的权重打包到一个64bit宽的存储字里这样一次读操作可以同时取出4个通道的对应权重。具体到CONV16个输出通道需要两轮每轮4个通道第二轮实际只有2个有效CONV2的16个输出通道正好4轮。这样权重读取带宽提升4倍配合16个PE的并行计算刚好能喂饱数据。打包读取带来的另一个好处是权重ROM的地址空间大幅缩小BRAM使用量也相应减少。整个LeNet的权重存储实际只占了约15KB的BRAM比我之前预估的60KB小了很多因为4路打包后有效存储密度提高了。4.2 输入特征图的缓存与双缓冲设计输入图像和中间特征图的缓存我采用了双缓冲ping-pong结构。所谓双缓冲就是准备两块等容量的BRAM区域一块在计算时被PE读取另一块同时被上游模块写入新数据。当计算完当前缓冲区的数据且新数据也写入完成时两个缓冲区的角色互换。为什么要双缓冲因为卷积层的计算和输入数据的加载是交错的。CONV1计算时需要先完整加载28x28的输入图像CONV2计算时输入数据是POOL1的输出而POOL1的输出是CONV1边算边产生的。如果没有双缓冲CONV2就必须等POOL1所有数据都写完后才能开始读中间有空闲周期有了双缓冲CONV1/POOL1正在向buffer A写入的同时CONV2可以从buffer B读取上一批数据两级流水完全重叠。双缓冲的代价是多消耗一倍的存储资源。这个项目里输入和中间特征图的最大单块存储需求是CONV1输出/POOL1输入的6x24x24x8bit 3.3KB双缓冲后6.6KBBRAM完全够用所以果断采用。具体的地址分配逻辑我单独写了一个 addr_gen 模块它维护每一层当前的读写指针。读指针和写指针的规律不同卷积层是行优先扫描池化层是2x2窗口跳跃式读取全连接层是顺序读取后广播。因为每层的寻址模式不同addr_gen内部用一个小的只读状态表来配置每层的行数、列数、通道数和步长。这样新增网络层时只需要扩展状态表不需要改动addr_gen的RTL代码。4.3 握手协议与控制状态机整个数据通路的数据传输我统一采用类AXI-Stream的valid-ready握手协议。规则很简单valid表示发送方数据有效ready表示接收方可以接收数据在valid和ready都为高时有效传输。这个协议的好处是天然支持反压。比如PE阵列的计算速度跟不上输入数据的产生速度时PE通过拉低ready通知上游暂停发送反过来PE计算速度够快时ready一直拉高数据流全速流动。控制逻辑只需要维护好每个模块的valid和ready信号不需要复杂的流控逻辑。控制状态机Ctrl模块是加速器的大脑我用一个主状态机加若干子状态机实现。主状态机有六个状态IDLE、LOAD_INPUT、RUN_CONV1、RUN_POOL1、RUN_CONV2、RUN_POOL2、RUN_FC、DONE。每个RUN_*状态内部再套一层子状态机遍历该层的所有计算单元。这里有一个设计取舍主状态机一次性把所有层算完还是每层由软件单独触发我选择了前者即只要给一个start信号硬件自动完成整个推理流程推理结束后拉高done信号。这样做的好处是软件交互简单缺点是中间不可打断。对于当前只做推理不接实时控制的场景全自动流程完全够用。调试时发现状态机里最容易出bug的是跨层状态切换的时序。比如CONV1计算最后一行的最后几个像素时POOL1的状态机已经开始工作此时RE LU模块输出可能还没有被池化单元完整消费。我在状态机里特意加入了层结束对齐机制每一层的结束信号不仅要等当前层计算完成还要等下游所有缓冲区的数据被完全消费。这个机制实现起来就是一组握手信号逐级传递务必保证前一层的数据全部被后一层接收后才允许主状态机进入下一层。5. 仿真验证与踩坑记录5.1 验证策略三级递进硬件加速器的验证策略我总结为三级递进RTL仿真验证、FPGA上板验证、软件对比验证。第一级是RTL仿真。用Vivado自带的仿真器写一个testbench模拟主机通过AXI4-Lite接口写入图像数据并读取推理结果。testbench里我会固定用几张MNIST测试图片每张图片同时用Python定点模型算出预期输出然后比对RTL仿真的输出向量。因为RTL仿真速度慢跑全量MNIST数据集不现实所以只选了10张代表性图片每类各一张重点验证各种边界情况比如全黑、全白、极端对比度。第二级是FPGA上板验证。通过JTAG加载bit文件后用Vivado的ILA集成逻辑分析仪抓取关键信号。ILA可以实时观测内部信号波形特别适合排查一些仿真环境里很难复现的外部输入时序问题。第三级是软件对比验证这个贯穿始终。设计过程中我维护了一个Python脚本实现与硬件完全一致的定点计算流程任何一层的计算结果都可以与硬件仿真结果逐像素比对。软件模型是参考标准硬件是被测对象只要两者输出一致就认为这一层的实现正确。5.2 实际调试中遇到的几个典型问题整个调试过程差不多花了两周踩了不少坑挑几个典型的记录一下。第一个坑是累加器位宽扩展不正确。最初累加器用的是16bit位宽想着中间结果传入激活函数前直接截断就行结果发现CONV2的累加值很容易溢出16bit。26通道乘以8bit数据再乘8bit权重加上偏置后最大绝对值超过了几千16bit有符号范围是-32768到32767看着够用但实际上某些极端输入组合下计算中间结果会更大。最后把累加器统一改成32bit问题解决。教训是累加器的位宽宁可多留不要省省出来的资源和排查溢出bug的时间完全不成比例。第二个坑是BRAM读延迟导致的时序错位。BRAM的读操作有一个时钟周期的延迟即地址有效后要过一个周期数据才出现在读数据端口。最初写卷积状态机时没有考虑这个延迟导致取到的权重和输入数据始终错开一个周期。排查方法是直接在仿真波形里对比读地址和读数据的对应关系发现错位后用寄存器缓存一拍数据或者把状态机跳转条件延后一拍解决。这个问题在RTL仿真里非常显眼一旦出现几乎所有计算都是错的。第三个坑是最大池化的边界条件。2x2池化窗口如果特征图宽高为偶数则没有边界问题但CONV1的输出是24x24POOL1后变成12x12CONV2输出8x8POOL2后变成4x4都是偶数理论上没问题。但我在最初设计时假设了通用情况即支持任意奇数尺寸结果边界判断逻辑写得很复杂反而引入了bug。后来简化成池化窗口必须完整覆盖才算有效由上一层保证输出尺寸匹配。这个取舍在实际使用中完全够用LeNet各层尺寸本来就是精心设计好的。5.3 性能结果与资源占用实测综合实现完成后我统计了资源占用和性能数据。资源方面XC7A35T的资源占用情况为 LUT约4200占18%FF约2800占6%DSP48E1用了32个占36%BRAM用了10块占20%。整体资源占用适中后续扩展其他网络结构还有余量。性能方面主频跑到了100MHz单张MNIST图像推理耗时大约0.8ms也就是约1250 FPS的吞吐率。这个数字已经远超软件CPU推理通常几十毫秒一张也快于大部分嵌入式平台对于这个规模的网络和这个档次的FPGA来说已经达到预期目标。功耗没做精确测量但按Artix-7典型值估算核心逻辑功耗应该不超过1.5W相较GPU方案的优势非常明显。5.4 常见问题速查表问题现象根因排查方法解决方案输出结果全部为0使能信号未同步或复位逻辑异常检查复位时序和使能信号波形统一使用同步复位使能信号寄存一拍输出结果偶发错误累加器位宽不足或BRAM读延迟未对齐比对Python参考模型与RTL波形累加器扩到32bit读数据寄存一拍某一层输出正确但下一层全错地址重映射逻辑覆盖了未消费数据检查状态机跨层切换时序增加层结束对齐握手信号权重读取偶尔错误权重ROM打包位序不一致检查权重排列与PE分发逻辑统一使用MSB优先的字节序排列性能明显低于预期握手协议反压频繁流水线气泡多统计valid-ready的有效周期占比增大输入缓冲深度优化状态机调度6. 从这次设计中沉淀的几点体会LeNet硬件加速器做到这里第一版基本能跑通全流程了。整个过程走下来最大的体会是硬件加速器设计真正难的往往不是乘累加阵列本身而是数据流的组织。计算单元再复杂也就是一堆DSP拼在一起真正考验功夫的是权重怎么排、地址怎么算、状态机怎么跳转、握手怎么处理。第二个体会是软件参考模型一定要先写先验证。我在写RTL之前用Python实现了浮点和定点两套推理模型后面所有RTL调试都拿定点模型当标准答案。没有这个参考模型很多bug我根本没法判断是硬件的问题还是自己期望值就错了。第三个体会是仿真验证不能贪多要精准。RTL仿真速度慢是硬伤全数据集跑一遍不现实。我的做法是挑边界样本、用定向激励配合ILA把关键路径波形看透比盲目跑大量数据有效得多。这个设计后续还有不少可以扩展的方向比如加入DDR存储以支持更大规模的网络、把PE阵列改成可配置的脉动阵列结构、引入Winograd快速卷积算法来降低乘法次数。当前版本的复用架构算是为这些扩展留了接口下一篇准备记录在ZYNQ平台上的PS-PL协同设计和DMA数据传输部分到时候把实测的端到端性能数据再详细整理出来。
返回列表