ARTICLE DETAIL

资讯详情

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

FPGA编译加速实战:从13小时到5小时的优化之路

FPGA编译加速实战:从13小时到5小时的优化之路 搞FPGA的人对“编译等很久”这事儿应该都不陌生。我手头有个ZYNQ图像处理高速接口的项目早期每次完整跑一版要13个小时白天改了代码晚上挂着编译第二天早上看结果如果时序还差点意思改两轮基本一周就过去了。后来我认真优化了整条编译链路把时间压到5小时左右迭代节奏完全不一样。这篇文章就围绕“从13小时到5小时”这个目标把FPGA编译加速的思路、操作方法、以及我踩过的坑整理出来主要针对 Vivado 流程但很多思路在 Quartus 里同样适用。1. 先搞清楚 FPGA 编译为什么这么慢1.1 编译流程里的时间都花在哪了FPGA编译不是单一操作而是好几个阶段串在一起尤其用 Vivado 的时候主要包括综合、实现里面又细分为逻辑优化、布局、布线和生成比特流。综合负责把 RTL 代码翻译成由 LUT、FF、DSP、BRAM 组成的网表实现负责把这些逻辑单元放到 FPGA 内部的物理位置上并且把信号连线走通最后生成 bit 文件去配置芯片。哪一步都有可能成为时间黑洞。从我实际看大型工程的情况来说时间主要烧在布局和布线上综合是第二大头。布局布线听上去只是“放位置、连线”但本质是解一个非常复杂的约束优化问题。工具有几万甚至几十万个逻辑单元需要摆放还要满足时钟频率、扇出、拥塞这些约束它需要反复尝试、评估、调整。逻辑规模越大、约束越紧工具尝试的次数就越多时间翻倍并不奇怪。有人可能会问为什么不能像写软件一样改几行代码马上看到结果原因是 FPGA 是物理实现你写的代码最终要落到芯片上的真实电路里。一个 LUT 放在哪、一根线从哪走都会影响最终的时序能不能收敛。所以这些计算基本都要做我们能做的是减少“不必要的重复计算”和“没有意义的优化尝试”。1.2 时间长的根源不只是资源量大资源量大当然是最直观的原因但我在实际项目里发现很多时候真正拖时间的不是资源量而是约束、代码风格、工具设置这些软因素。最常见的坑是“约束不完整全靠工具猜”。比如没有把所有的时钟都定义清楚没有给跨时钟域的路径做 set_false_path工具就会用很保守的方式去检查这些路径甚至为了确保收敛做大量额外尝试run time 一下就上去了。反过来的另一种坑是“过约束”把本来不需要那么高频率的路径约束到一个几乎不可能收敛的时钟频率工具就会花很长时间去尝试布线最后还失败了。代码风格也影响很大。如果你把一大段组合逻辑写在一个 always 块里综合器很难在面积和时序之间做平衡逻辑级数变深布局布线阶段就要花更多力气去优化。这类问题属于源头问题不是光靠加机器就能解决的。回到我那个13小时的工程最严重的问题其实是流程太“默认”每次都是全量编译而且一堆报告生成选项都开着实际上很多报告跑完根本没人看。2. 一个等13小时的工程到底长什么样2.1 项目背景ZYNQ图像处理高速接口这里简单交代一下当时工程的大致情况方便你对“13小时”有个概念。这个项目用的是 ZYNQ UltraScale 平台PL 端做的事情包括接入 HDMI 输入、做多级图像预处理和边缘检测算法、通过 DMA 和 PS 端交互、再走 PCIe 或千兆以太网把结果传出去。为了吞吐内存接口肯定绕不开DDR4 控制器带多个 AXI 端口还有不少跨时钟域处理的逻辑。整片资源利用率大概在 60%~70% 之间。这个利用率其实不算特别夸张但由于图像处理的流水线很深、接口协议又复杂综合和布局布线的压力都很大。再加上项目中既有 AXI 总线的时序约束又有视频时钟、DDR 时钟、PCIe 参考时钟等多组时钟约束稍不注意就出现时序违例。我当时最崩溃的一次是只加了一个 Debug 核想抓内部信号看波形结果整个工程重新编译又多等了13小时。抓完信号之后其实代码只改了一行触发条件又要等13小时。那段时间基本就是白天改代码晚上等编译效率非常低。2.2 最初的编译设置和瓶颈分析这个工程在优化之前用的是 Vivado 默认的设置GUI 模式启动综合和实现全部默认策略每次 launch_runs 都是 from scratch 全量跑实现的 strategy 用的 Balanced完全没开增量编译也没有特意指定多线程。瓶颈分析下来其实可以分成三块第一综合阶段没有用多线程优化CPU 多核基本没跑满综合本身花掉两个半小时。第二布局布线环节默认的 Balanced 策略会做大量的时序优化探索为了追求更好的 QoR工具会尝试很多种摆放和布线方式这个阶段花了将近9个小时。第三跑完后还要生成一堆报告包括时序、利用率、功耗、时钟交互等等有些报告我平时根本不看但每次默认都会生成这部分零零散散加起来也占了快一个小时。你可能会觉得13小时已经够惨了但更惨的是如果只改了一点点 RTL这一整套流程还是从头跑到尾完全没有复用之前已经跑好的结果。这里就引出了后面要讲的重点加速的关键不是买更强的机器而是让工具少做无用功。3. 从13小时到5小时我实际用到的提速手段3.1 先把机器和流程基础调对如果条件允许优先确认编译服务器硬件配置。这里的顺序很重要CPU 核心数和内存是基础SSD 是惊喜。FPGA编译是典型的多核并行大量临时文件读写场景核心数不够后面怎么调策略都有限内存不够多线程甚至会因为换页变得更慢。我建议大工程至少 8 核 16 线程配合 32GB 以上内存。实际体验下来16 核 32 线程的服务器编译速度相对8核有明显提升但如果只是4核带小内存开多线程反而可能更卡。硬盘强烈建议 SSD因为布局布线中间会频繁写 checkpoint、读约束文件、生成报告机械盘在这些小文件随机读写上非常吃亏。另外尽量不要用 GUI 跑大工程。Vivado 的 GUI 本身会吃掉大量内存和 CPU特别是工程特别大时GUI 里的原理图、设备树、逻辑分析仪窗口都在后台维护状态。我现在要么用 batch 模式要么用 Tcl 脚本一键跑这样可以把资源全部留给编译本身。3.2 用多线程和策略选项让工具干活更高效在确认硬件没问题之后我做的第一件事就是把 Vivado 的多线程参数打开。不同版本设置入口略有差异但我以常用的 2020.2 版本为例综合阶段可以在 Project Settings - Synthesis - Options 里找到 Number of jobs把它从 1 改成 4 或 8实现阶段可以通过 Tcl 命令设置 place 和 route 的最大线程数比如set_param place.maxThreads 4 set_param route.maxThreads 4这类参数在旧版本里可能叫 maxThreads新版本可能在图形界面直接有选项本质都是让布局布线算法用多线程并行处理分区。我实测下来综合从 2.5 小时降到 1.5 小时左右布局布线也明显快了一些。再就是策略选择。Vivado 的实现策略里Balanced 是默认但如果你只是想快速验证功能完全可以选 RuntimeOptimized它会大幅减少工具在时序优化上的尝试次数。我当时把 strategy 从 Balanced 切到 RuntimeOptimized布局布线时间从 9 小时降到 4 小时出头这个收益非常直观。不过这里有一个非常关键的权衡点RuntimeOptimized 通常会让 QoR 变差如果本来时序就非常紧可能导致实现无法收敛。所以我的用法是早期功能验证、改 RTL 逻辑、加调试逻辑的阶段用 RuntimeOptimized真正要到流片或最终验证时序的时候再切回 Performance Explore 或者多跑几个 strategy 去挑最好的结果。3.3 增量编译的正确打开方式除了策略另一个大杀器是增量编译。它的原理简单说就是把上一步已经跑好的布局布线结果作为参考这次编译只重新处理你有改动的部分其他部分尽量复用原来的布局和布线。这样改动小的时候时间可以从十几个小时直接压到一两个小时。在 Vivado 里启用增量编译需要提供一个 reference checkpoint一般是上一次实现的 DCP 文件。GUI 路径大致是 Project Settings - Implementation - Incremental Compile设置 reference checkpoint或者在 Tcl 脚本里指定属性。但这里有个前提上一版编译必须成功完成而且 DCP 要保留好。我后面把流程改成代码稳定后的日常微调全部走增量编译只有代码结构大改或约束大改时才重新全量编译。这样大部分改动只要几分钟到几十分钟就能看到结果。注意如果你 RTL 改动涉及模块的层级、端口和接口协议增量编译带来的速度提升会明显减弱甚至可能因为前后矛盾而失败这种情况就老老实实全量吧。3.4 脚本化流程减少GUI和报告带来的额外开销很多人在 GUI 里点“Run Synthesis”“Run Implementation”觉得很方便但大工程里我建议切换到非项目模式non-project flow或者 Tcl 脚本模式。好处有几个第一省掉 GUI 自身的开销。第二可以精确控制每个阶段做什么比如只综合、只布局、只布线不需要每次都生成全部报告。第三更容易做自动化批处理可以用定时任务晚上跑版本第二天早上收结果。基于 Tcl 的最小化流程大概长这样read_verilog [list top.v module_a.v module_b.v] read_xdc constraints.xdc synth_design -top top -part xc7z045ffg900-2 -jobs 8 opt_design place_design phys_opt_design route_design write_bitstream -force output.bit这只是一个示意实际工程还会涉及 IP 核、宏文件、多个 XDC 等。但核心思路是让每一步都按照你的命令行来不用的报告别生成不需要的进程别启动。我当时把默认的报告生成选项关掉了一大半只留下最关键的时序报告和利用率报告这部分又省出几十分钟。如果你平时还需要抓信号调试可以只在调试阶段加上 Debug 核和波形导出平时编译一个“干净版”和“调试版”分开跑别让调试逻辑跟着每次编译都走一遍。这个习惯帮我省下非常多时间。4. 加速时最容易踩的坑帮你提前避开4.1 增量编译不是万能的改大结构时别用增量编译最大的坑就是“你以为自己在做增量其实工具在给你全量重跑甚至更慢”。我遇到过几次情况只改了一个小模块但增量编译跑下来比全量还久。查到最后发现是因为我把不少约束文件也一起改了导致所有约束相关的路径都要重新评估增量效果被抵消。还有一次参考 checkpoint 用的是很久以前的版本中间 IP 版本升级了增量编译直接报错。后来我定了一个规矩每次发布一个稳定版本后立即导出一份干净的 DCP 和工程归档作为后续增量编译的基准。如果改动点涉及多个模块或者大规模重构果断全量不要在增量上死磕。4.2 追求速度牺牲时序收敛需要根据阶段灵活取舍RuntimeOptimized 策略虽然快但 QoR 通常不如 Balanced更不如 Performance Explore。具体表现是同样的 RTL用 RuntimeOptimized 可能布线更绕关键路径变长最终 timing 是 met但 slack 很小如果需求有波动再多加一个 IP 核很可能就收敛不了。我踩过的一次是为了赶版本用了 RuntimeOptimized时序报告显示 WNS 刚刚满足自己也觉得稳了结果加了几个 DDR 读写时序约束之后整个路径全乱了布局布线彻底失败。后来我再也不在整个流程从头到尾都用 RuntimeOptimized而是把定时长、压力小的探索性实现放在晚上用 Performance Explore 跑白天小改直接用 RuntimeOptimized 快速确认功能。简单说速度策略适合“探索期”性能策略适合“收敛期”。一定要在每个阶段结束时记录当前策略下的 QoR方便以后回溯。4.3 提速后必须做的事对比综合与实现结果提速手段用得多很容易忽略一个重要问题不同策略、不同线程数、增量编译都会改变最终布局布线的选择有时还会带来资源利用率和时序的微妙变化。所以我养成了一个习惯每次换策略或者换加速方式后都会做一次“新旧结果对比”。对比的指标主要是这几个WNS/TNS关键路径 slack 有没有变化。逻辑级数太多级数组合逻辑常常是性能瓶颈。拥塞度看 LUT/BRAM/DSP 分配是否均匀拥塞严重的区域往往会让布线阶段反复尝试。资源利用率有没有因为策略变化导致某些资源异常增高。这些数据在 Vivado 里通过 report_timing_summary、report_utilization、report_qor_assessment 就能拿到。遇到异常第一时间检查是不是策略放宽导致某些路径优化不到位或者增量编译复用的布局和当前代码产生了不一致。5. 常见问题排查与更多提速思路5.1 编译加速常见问题速查我把这段时间常见的问题整理成一个表格方便你对照排查现象可能原因解决方法开了多线程但时间反而变长内存不足导致换页或旧版本线程调度不理想加大内存检查任务管理器确认 CPU 占用适当减少线程数增量编译后时序变得更差参考 checkpoint 与当前设计改动不匹配尽量保证增量前的小版本稳定避免大量约束和 RTL 同时改动RuntimeOptimized 下时序不满足策略本身以速度优先性能优化有限最终版本切回 Balanced/Performance 策略或针对关键路径单独约束关闭报告后调试困难缺少时序和利用率数据不知道改动影响只保留最重要的 timing summary 和 utilization其他报告临时需要时再手动跑非项目模式下脚本报错Tcl 流程控制不熟练缺少依赖文件先从 GUI 中导出 Tcl 脚本再改逐步加入自己的步骤每隔几次编译结果差异大使用了随机种子策略导向不一定稳定固定 seed或在多 strategy 中横向对比挑最优5.2 更进一步的思路分布式编译和定制IP如果你已经把手头的单机手段用完了还想继续缩短时间那可以考虑分布式编译。Vivado 本身对多机并行支持不算友好但社区和商业工具里有基于任务调度的分布式方案。思路通常是先把综合任务拆成多个子模块分发给多台机器并行综合再汇总结果。理想情况下能明显压缩综合时间但布局布线阶段因为要处理全局资源和时序一般不太容易并行。对于个人或小团队我觉得分布式编译的投入产出比不高它更适合固定版本的回归测试比如每天晚上跑多个配置、多个 seed、多个策略的矩阵。开发阶段的随机性和迭代频繁程度反而更适合用单机多线程 增量编译来解决。另外如果你有大量定制 IP可以提前把它们生成好并缓存下来避免每次全量编译时重新综合这些 IP。Vivado 的 IP 输出产品支持缓存和复用特别是像 PCIe、DDR Controller 这种很大的 IP重新生成一次可能就要十几分钟缓存之后能省下一部分时间。5.3 项目小改动如何让日常迭代更快很多时候编译时间久不完全是工具问题而是工作习惯问题。我现在会刻意把“大验证”和“小改动”分开。小改动阶段比如改了一个图像处理算法的系数或者调整了一个 AXI 数据位宽这些改动通常不会影响全局布局我会直接走增量编译并且用 RuntimeOptimized 快速跑通。只要功能对、没有明显时序违例就先往下推进。大验证阶段比如 RTL 结构重构、时钟域调整、需要评估极端情况下能不能收敛我会在晚上运行全量编译并使用 Performance Explore 或者多策略并行。第二天早上看结果然后根据时序报告决定是调整代码还是调整约束。这样分层的思路本质上是在保证质量的前提下让工具该快的时候快该稳的时候稳。6. 关于编译加速我觉得真正重要的几件事如果你完整看到这里应该能感受到FPGA 编译加速不是某一招就见效而是一套组合拳。硬件、多线程、策略、增量编译、脚本化、良好的分层迭代习惯每一项单独拎出来可能只省一两个小时但加在一起13小时变5小时是完全能实现的。而且这套组合拳帮我省的绝对不只是“等待时间”还有整个团队的迭代信心。我个人的体会是编译时间这种东西平时不觉得是问题真正卡住的时候才意识到它有多消耗团队心智。我曾经因为一次完整的编译失败重跑了整整一个周末之后就下定决心优化整个流程。现在的流程里我会在每次改动较小的时候尽量走增量编译在需要确定性收敛的时候切回性能策略同时盯住 WNS、资源利用率和拥塞度这几项关键指标。最后再分享一个小技巧每次跑编译之前先看看这次改动的边界在哪。如果只是一个小模块别动不动让整个工程全量重来如果你只是调了一处约束也可以手工指定只重新综合和实现的必要步骤。多花几分钟想清楚流程比盲目启动 launch_runs 然后干等13小时有用得多。
返回列表