ARTICLE DETAIL

资讯详情

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

FPGA编译时间优化:Vivado增量编译与约束设置实战

FPGA编译时间优化:Vivado增量编译与约束设置实战 1. 13小时到底浪费在哪——先看清编译瓶颈的分布先说结论我那个工程从13小时压到5小时不是靠换电脑不是靠加内存更不是靠运气。是靠把编译这件事拆开看搞清楚时间花在了哪个环节然后针对性地改工作流。这个工程是什么样的呢Zynq UltraScale MPSoC差不多用了20万左右的LUT里面有PCIe DMA通路、图像采集与预处理、DDR4读写、好几条跨时钟域的数据管道外加MIPI和HDMI接口。这种规模在FPGA项目里不算最大但也绝对不小了。早期我用的是Vivado默认流程每次改完代码点一下Implementation然后就去干别的。结果就是早上提交编译晚上下班还没跑完第二天早上过来看结果再改——一天只能迭代一次。13小时这个数字不是我编的是某次比较大的改动后完整跑一遍的实测时间。而且这还只是Implementation布局布线的时间不算综合。13小时意味着什么意味着你改一个跨时钟域的同步逻辑想验证一下亚稳态处理对不对得等一整个白天意味着你发现时序违例是因为某条路径的约束写错了改完还得再等一个轮回意味着整个团队只要有一个大改动的版本在编译其他人想插队跑个小验证基本没戏。这种节奏下项目怎么可能按期推进。先说为什么默认流程会这么慢。Vivado的Implementation分成几个阶段opt_design优化、place_design布局、phys_opt_design物理优化、route_design布线。对于一个大工程布线永远是最大的时间黑洞因为布线器要在整个芯片范围内寻找可行的走线方案遇到拥塞或时序紧张的区域它还要反复绕线、重试、换路径。物理优化也相当耗时它是在布局之后对时序违例路径做修复有时候会进行多轮次迭代。那是不是换一台更强的机器就能解决有一定帮助但收效有限。Vivado是多线程工具但它对CPU的利用率并不是线性增长的到一定线程数之后收益就明显递减。而且大工程在布线阶段的内存带宽压力很大你换一个更高主频的CPU不如调整编译策略来得实在。真正有用的思路是四个字减少重算。我后面优化到5小时本质上是让工具在第二次、第三次编译时不要把所有东西从头再来一遍。下面我会把每一步具体怎么改、为什么这么改、改完效果怎么样全部展开来讲。2. 从13到5的第一次跃迁把综合模式改对很多人一提到编译优化就直奔增量编译但实际上综合阶段的设置如果不对增量布局布线的收益会大打折扣。我先从综合阶段讲起。2.1 flatten_hierarchy为什么它是时间杀手Vivado综合器默认的hierarchy mode是full也就是全局扁平化综合。这是什么意思呢就是说工具会把整个设计的层次结构全部打散把不同模块里的逻辑重新混合优化跨模块边界做资源共享、寄存器合并、逻辑复制等。这种模式的优化效果通常最好面积和性能都能得到较优的结果。但是代价就是任何一丁点代码改动哪怕只是改了某个子模块里的一个条件判断整个设计的综合结果都可能面目全非。因为层次已经被打散了一个模块的改动可能影响到其他模块的综合策略导致所有模块都要重新综合。这在编译时间上就是灾难。我把综合设置里的flatten_hierarchy从full改成了rebuilt这个操作很关键。rebuilt模式的意思是保留层次结构综合时每个模块单独优化然后在顶层把它们组装起来。这个模式下的综合结果跟full模式相比面积可能会有少量增加一般在5%以内但带来的好处是模块之间的综合结果相对独立改了一个模块其他模块的综合网表可以保持稳定。这个改动对后续的增量编译有决定性的影响。如果层次被扁平化那个增量的意义就大打折扣因为布局器每一次看到的网表结构都可能是新的如果层次保留下来布局器就能相对容易地找到上一轮编译中对应模块的位置和走线方式。2.2 增量综合的正确配置方法Vivado的增量综合本质上是利用上一次综合生成的网表和缓存信息指导本次综合在相似逻辑上复用之前的综合结果。但它不是简单地把旧结果拿来用而是有一个引导guide的机制。在GUI上的操作路径是Project Settings → Synthesis → More Options勾选incremental选项同时指定上一次的综合运行synthesis run作为参考。如果用Tcl命令是这样的set_property STEPS.SYNTH_DESIGN.ARGS.INCREMENTAL true [get_runs synth_1]然后启动综合。增量综合跑完后会在工程目录下生成一个.dcp文件这个文件包含了综合后的网表和相关的逻辑映射信息。这个文件也是后续增量布局布线的基础。很多人只关注布局布线的增量忽略了综合的增量。但实际上如果综合阶段每次都把网表从头生成一遍网表的逻辑连接关系、模块划分、寄存器位置映射都变了布局增量根本没法有效地参考上一轮的结果。所以综合增量一定要开。2.3 综合阶段的时序预估不要乱开还有一个容易被忽视的选项综合时的timing summary估算。Vivado在综合阶段会做一个粗略的时序分析将估算结果反馈给用户。如果你在GUI里勾选了综合后自动打开timing summary工具会在综合结束后跑一次完整的时序评估。这个功能本身不慢但它会引导很多人形成坏习惯——看到综合后的时序报告有违例就开始调整代码然后再综合再评估。说实话综合阶段的时序估算跟真实布局布线后的时序差距很大因为这个时候还没有真实的布局、真实的走线延迟完全是靠模型估算的。我见过很多人花大量时间想消除综合阶段的时序违例实际上这大部分是徒劳的。正确的做法是只要综合阶段的违例不是大范围、数量级的违例就直接进入布局布线以最终时序报告为准。还有个更耗时的选项是综合时的retiming。retiming会把寄存器在组合逻辑中间挪来挪去用来优化时序。这个功能在某些场景下很好用但它的副作用是即使很小的代码改动在retiming作用下都会导致大量寄存器位置变动这几乎会把增量综合的复用率清零。如果工程项目对编译时间敏感我建议在综合设置里把这个功能关掉。3. 增量编译的正确打开方式原理与适用边界增量编译是这次优化的核心手段也是网上讨论最多、误解最多的一块。我很负责任地说一句增量编译不是一个开关它是一套方法论。用对了时间真的能砍半用不对它可能给你一个编译通过但行为异常的结果比编译失败更危险。3.1 布局布线的guide机制是怎么工作的增量布局布线的原理我打个比方你有一张桌子上一轮编译已经把东西都摆好了。这轮编译你只是把某个抽屉里的东西换了一下那你当然不想把所有东西都重新摆一遍。guide机制就是让工具记住上一轮每个逻辑单元在芯片上的位置、走线的路径然后在这轮编译时尽量把相关的逻辑放在相近的位置。Vivado里启用增量布局布线的操作是在Implementation Settings里勾选incremental然后在旁边指定参考的checkpoint也就是上一轮布局布线完成后生成的.dcp文件。用Tcl的话是这样set_property STEPS.PLACE_DESIGN.ARGS.INCREMENTAL [get_runs impl_1] # 或者是通过place_design -incremental / route_design -incremental直接指定实际操作中我习惯直接在Tcl Console里跑流程因为这样可以看到每一步的日志输出遇到问题也方便回溯。流程大概是open_checkpoint ./checkpoints/post_route_20241023.dcp place_design -incremental phys_opt_design route_design -incremental write_checkpoint -force ./checkpoints/post_route_new.dcp这里有个关键点增量布局布线能不能生效取决于两个checkpoint之间的差异程度。上一轮编译和这一轮编译如果只是改了某个模块的内部逻辑布局器会尝试把新旧逻辑映射到接近的位置然后在这个区域做局部调整。这就是增量编译省时间的核心。3.2 什么改动会毁掉增量优势这个必须说清楚否则会踩大坑。增量编译并不是在所有改动下都能加速。先说清楚哪些是安全的改动修改模块内部的功能逻辑、增加或删除少量寄存器、调整组合逻辑的层级、优化状态机的编码方式这些属于“局部改动”增量布局布线的效果很好。哪些是危险的改动顶层模块的端口列表变了、IP核的配置变了比如DDR4控制器的参数调整、器件型号换了、综合策略变了比如从rebuilt改回full、SDC约束的时钟结构大改了。这些改动会让新旧checkpoint之间的逻辑结构差异巨大增量guide机制不但不能加速反而可能因为需要不断匹配新旧逻辑关系而变慢。更严重的情况是如果你的时序约束里对某些路径的名字做了修改或者把某个时钟域的名字改了增量编译时工具找不到对应的逻辑节点就会把这块逻辑当作新逻辑重新布局布线。这个区域如果恰好是时序收敛的瓶颈区域那你这次编译基本等于全量重跑甚至可能因为guide过程中的额外开销而比全量更慢。所以我的建议是建立工程规范。每次运行增量编译前用diff工具检查一下自上次编译以来修改了哪些文件、每个文件改动的大小。如果发现SDC文件变了或者IP核配置变了就直接跑全量编译不要指望增量。3.3 保守一点先验证再增量还有一个特别重要的习惯在大改动之后在部分改动但工程量很大的情况下先做一次全量编译生成一个新的baseline checkpoint然后再在这个baseline上做小改动的增量编译。这样做的好处是你有一个干净的参考点。我经历过的教训是有一次我在一个旧的checkpoint上连续做了三轮增量编译每次改动都不大编译也确实很快。但到第四轮的时候需要改一个跨时钟域逻辑结果编译完成后出现了一个奇怪的时序违例这条路径在之前几轮编译里从来没违例过。排查了很久最后发现是连续增量编译导致工具在某些路径上积累了太多guide的历史信息布局结果已经偏离了最优状态。从那以后我的规则就变成了每五次增量编译之后或者每两轮大改动之后主动做一次全量编译来校准baseline。这个习惯看着保守实际上是省时间的。4. 模块级复用与OOC综合把大盘子拆小增量编译解决的是“在大盘子基础上做小改动”的加速问题。但如果你的项目经常有大范围的改动比如要新增一个功能模块或者重构某个子模块的数据通路那增量编译的收益就会打折扣。这个时候模块级的综合复用和布局约束就派上用场了。4.1 OOC综合的缓存复用机制Vivado里有个概念叫Out-of-ContextOOC综合直译就是“脱离上下文”的综合。默认情况下IP核都是OOC综合的也就是说每个IP核单独综合生成独立的网表和时序约束然后在顶层组装时直接引用。这样做的好处是改了一个IP核的配置只需要重新综合这个IP核其他模块不受影响。我们自己的模块也可以设置成OOC模式。Vivado里的做法是为每个要OOC的模块创建单独的synthesis run并设置这个run的top为对应的模块名。用Tcl可以这样create_run ooc_mydesign -flow {Vivado Synthesis 2020} -strategy Vivado Synthesis Defaults set_property top my_design [get_runs ooc_mydesign] set_property used_in_synthesis true [get_files my_design.dcp]放在综合完成后会在工程目录下生成一个针对该模块的.dcp文件。顶层综合时工具会直接把这个.dcp引入而不会重新综合这个模块。这个机制的好处很直接如果一个模块很大但是独立性强典型的就是DDR控制器、PCIE硬核的接口逻辑、图像处理流水线把它设为OOC之后即使顶层有大量改动这个模块的综合结果也能直接复用。综合时间就能省掉一大块。4.2 什么模块适合OOC什么不适合并不是所有模块都应该OOC。我总结了一些经验和判断标准。适合OOC的模块特征有独立的时钟域比如DDR控制器、PCIE逻辑接口信号比较多、内部逻辑复杂单独综合耗时很长功能相对稳定不经常改动时序约束相对独立有自己的input/output delay约束不适合OOC的模块特征与顶层其他模块有频繁的跨模块数据交互综合时需要全局视角做优化经常和顶层其他模块一起调整时序逻辑规模较小单独综合收益不高OOC的核心问题在于它牺牲了跨模块的全局优化能力。一个模块如果在综合时看不到外部环境的约束它的优化方向可能和整体设计不一致。比如一个数据通路模块它内部的逻辑深度优化得很好但外层和它衔接的模块时序吃紧需要这个模块让出部分性能来做适配OOC模式下就很难做到这一点。我一般只对IP核和比较成熟的、不太改动的子模块设置OOC对于正在快速迭代的核心逻辑还是保持正常的层次综合。4.3 Pblock逻辑锁定用空间约束换编译时间还有一个和增量编译配合起来很好用的手段是Pblock逻辑锁定。Pblock允许你把某个子模块的逻辑锁定在芯片的特定区域内。这样做的好处有两点第一布局器不需要在整个芯片范围内为这个模块搜索位置搜索空间缩小布局速度提升第二因为模块被锁定在一个区域这个模块的走线相对集中对周边模块的干扰减小跨编译的布局结果更容易保持一致这对增量编译非常友好。设置Pblock的命令大概是create_pblock pblock_dma add_cells_to_pblock [get_pblocks pblock_dma] [get_cells dma_inst] resize_pblock [get_pblocks pblock_dma] -add {SLICE_X40Y100 SLICE_X75Y180}Pblock的布局范围需要根据模块的大小和资源占用情况来设定。可以先用一次全量编译后的物理资源报告utilization report看这个模块实际用掉的资源分布范围然后在此基础上留出15%到20%的余量再设置Pblock范围。范围设得太大锁定的意义就不大设得太小布局器在区域内塞不下反而会产生大量穿越Pblock边界的走线时序就崩了。5. 约束质量是编译时间的隐形杠杆这块内容很少被放在编译优化的文章里但我实际做下来发现约束质量对编译时间的影响巨大甚至超过了某些工具设置。为什么因为时序约束本质上是给布局布线器设定了一组“必须达到的目标”目标不合理工具就会在布线阶段反复尝试、反复迭代时间全耗在里面了。5.1 无效时序路径如何拖慢布线早期我接手一个项目工程规模不大但布线时间异常长。后来检查发现设计里有个模块用了异步复位复位信号是在外部引脚引入的。因为我在SDC里没有对这个引脚做set_input_delay约束工具不知道这个外部信号到达芯片内部的时间窗口于是它假设这个信号可能在任意时刻变化然后对所有使用该复位信号的时序路径都要求进行时序分析。结果就是几乎每一条路径都被当作需要严格收敛的时序路径来处理布线器压力巨大。后来我在SDC里加上了对应的输入延迟约束并把复位相关的路径设置成false path布线速度立刻就有明显提升。这是一个经典的错误但也是一个很好的例子约束不是越少越好也不是越多越好而是要准确。对于那些真正不需要时序收敛的路径你要明确告诉工具。工具知道得越准确就越不会做无用功。5.2 时钟分组与异步路径的显式声明FPGA设计里最典型的耗时来源是跨时钟域逻辑。如果你的设计里有多个异步时钟域而且这些时钟域之间确实有数据交互同步器处理过的你需要在约束里用set_clock_groups声明它们是异步的这样工具就不会费劲去分析这些跨时钟域路径的建立保持时间。命令示例set_clock_groups -asynchronous -group {clk_pix} -group {clk_sys} -group {clk_ddr}如果设计中存在多主时钟的case比如同一个PS端的参考时钟经过MMCM产生了主时钟和辅助时钟时钟结构理清楚也是关键一环。我曾经见过一个工程因为时钟树定义混乱工具不得不在布线阶段对大量无效路径做时序分析整整多花了一个多小时。还有一个高频考点引脚上的复位信号。如果你用了按键复位外部复位信号没有做异步复位同步释放那它对内部所有时序路径都是一个异步输入。正确做法是把这些复位相关的路径显式声明为false pathset_false_path -to [get_pins [list rst_sync_reg/C]]为什么要这么做因为工具默认是悲观的它会尽可能分析所有与时序有关联的路径。只有你主动告诉它哪些路径不用分析它才会省下这个力气。5.3 你可能没意识到的一个坑物理约束的冗余循环在布局布线过程中物理优化phys_opt_design阶段会尝试通过调整寄存器的位置来修复时序违例。这个过程在时序紧张的设计里可能会反复执行多轮。如果时序报告里的违例路径集中在某个区域内phys_opt一般只会在这个区域做调整速度上还可以。但如果违例路径分散在整个芯片上phys_opt会像打地鼠一样四处调整耗费的时间会大幅上涨。这种情况往往不是工具的问题而是某些关键路径的约束设计不合理。比如你给某条总线设置的input_delay过大导致所有经过这条总线的路径都紧张phys_opt就需要在多个地方同时做手术。这种情况我建议回到约束源头去排查而不是指望物理优化来解决根本问题。很多时候少吃两轮phys_opt的苦就是编译加速的最好贡献。6. 完整实测一份从13小时缩到5小时的修改清单说了这么多原理总得来点实际的。下面是我在那个工程上实际做的改动、顺序和最终效果。列出来供参考但一定要结合你自己的工程情况调整。6.1 我实际改了什么按优先级排列第一步修改综合设置。这一步我把综合模式从hierarchical full改成了hierarchical rebuilt。注意没有改得太激进因为优化效果还是要保证的。同时关闭了综合后的自动时序报告生成减少综合阶段的多余计算。第二步启用增量综合。设置当前的综合run为增量模式并指定参考运行。这一步需要在综合运行前完成并且需要在综合运行配置里指定Reference Run。这个值可以在重新综合时动态调整。第三步检查SDC的完整性。这个我花了一个下午仔细做但效果立竿见影。把所有跨时钟域的路径用set_clock_groups声明为异步把复位相关的IO路径设成false path检查所有input_delay/ouput_delay约束是否与外部时序要求一致删掉了一些过时的、不再生效的约束行。经过整理之后我在综合阶段和布局阶段的时序分析日志里能明显看到约束冲突的警告数量大幅下降。第四步建立新的baseline checkpoint。把上述修改做完之后我跑了一次完整的全量编译。这次的编译时间已经从13小时降到了8小时左右主要省下来的时间在physical optimization阶段因为时序约束清晰了phys_opt没有那么多要修复的路径了。第五步开始使用增量编译。在这个8小时的baseline基础上后续的小改动跑增量编译大部分在2到3小时内完成。如果仅仅改了一个模块的内部逻辑且改动量不大最快的记录是1小时40分钟。第六步对稳定的大模块设置Pblock并限制它们的布局范围。这一步是在有新版本需要全量编译的时候才引入的短期的编译时间又会进一步压缩。最终某次需要全量编译的版本跑到了5小时10分左右。6.2 编译时间对比与资源、时序代价我整理了一下这个优化过程的实测数据但注意这些数据依赖具体的工程规模、器件型号、约束情况可能和你遇到的场景不完全一致仅供参考。阶段综合耗时布局耗时布线耗时总耗时不含综合备注原始默认流程约45分钟约2.5小时约8小时约13小时布线和phys_opt耗时最长综合模式约束优化后全量编译约40分钟约2小时约5小时约7.5小时主要节省在phys_opt和布线优化后的增量编译小改动约15分钟约40分钟约1小时约1.5-2小时模块改动小的情况下效果明显优化后的大改动全量编译含Pblock锁定约45分钟约1.5小时约3.5小时约5小时依赖模块划分是否清晰资源方面flatten_hierarchy从full改成rebuilt后LUT使用量增加了约2%FF使用量基本持平。这个代价在资源不紧张的工程里完全可以接受。时序方面同样的约束下全量编译的时序结果比之前的默认流程略微变好可能是因为约束整理后工具建立了更准确的目标增量编译的时序结果与全量编译基本一致部分的布局区域有些微变化但都没有导致最终的时序违例。6.3 后续还能继续压缩的增量空间这套组合拳打完5小时基本成了我这边全量编译的常态。但如果项目规模再扩大需要更进一步我还有一些可操作的手段。一个是物理优化策略的定制。Vivado默认的phys_opt策略偏保守会做很多轮迭代。如果你的设计时序余量大可以试试把phys_opt的优化目标改成快速收敛模式或者直接精简掉部分冗余的优化环节。这可以通过自定义Implementation策略来实现。另一个是深入使用Partial Reconfiguration部分重配置的思路。虽然不一定用到运行时重配置功能但PR的设计方法论本身就有价值它天然要求把设计划分成几个相对独立的模块。如果把这种划分方式反向应用到普通编译流程里配合OOC和Pblock编译效率还能再往上涨。还有一个很容易忽略的点Vivado版本。我后来从2019.2升级到2021.1之后发现同款工程在布局布线阶段的算法有明显改进编译时间又快了一截。所以如果条件允许定期升级工具链也是一个潜在的加速手段。这些手段我没有全部在当前工程里用上因为5小时的编译时间对这个项目的迭代节奏已经够用了。但如果你的项目规模更大比如需要用到UltraScale里最大的器件或者系统的逻辑规模到了30万LUT以上这些高级手段大概率会派上用场。最后再说一个实操中的小细节增量编译不是万能的但它是一定要开的。哪怕是全量编译也建议定期保留checkpoint这对快速定位问题和回归验证都有好处。我在当前工程里已经养成了习惯每次编译跑完把分析后的checkpoint和综合后的dcp都归档保存。这样后续调试问题时不需要重新编译直接打开checkpoint就能查看内部信号节省的时间远超维护这几个文件带来的负担。
返回列表