ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA编译加速实战:Vivado增量编译将13小时缩短至5小时

FPGA编译加速实战:Vivado增量编译将13小时缩短至5小时 FPGA圈子里有个段子干这行的人一天只干两件事写代码和等综合。写代码半小时等编译大半天尤其是到了项目后期逻辑资源用掉百分之七八十、布局布线处处受限的时候一次完整跑完十几小时很正常。我手上这个项目就是典型Zynq UltraScale 平台逻辑用了差不多 72% 的 LUT时序非常紧为了修一条关键路径上的建立时间我改了 RTL 里两行逻辑。就这两行完整跑一次布局布线要 13 个小时。13 小时意味着我下午五点提交第二天早上九点半才能看到结果一晚上就这么没了。要是时序没过再改一行再等一个通宵。那段时间我整个人都是被编译牵着走的白天改代码晚上睡觉梦里都是 timing report。后来我实在受不了了开始系统性研究 FPGA 编译加速这件事把能试的路子都试了一遍。现在同一个工程小改动基本能控制在 5 小时左右跑完遇到大改动也不至于全盘重来。这篇文章就是把我踩过的坑、试出来的有效方案、以及背后的原理一次性说清楚希望能帮被编译时间折磨的兄弟们省下几个通宵。1. 先搞清楚时间都花在哪里了1.1 综合 vs 实现谁才是真正的耗时大户很多人一上来就想着怎么优化连时间花在哪都没弄清楚。FPGA 的完整编译流程大体分两步综合synthesis和实现implementation。实现里边又细分翻译、映射、布局、布线其中布线route几乎永远是最耗时的那一环。我拿自己的工程做过一次完整时间统计13 小时的总耗时里综合大概花掉 1 小时 40 分逻辑优化和映射加起来大概 1 小时出头布局place接近 2 小时剩下的 8 小时以上全部被布线吃掉了。布线为什么会这么慢因为布线器面对的是一个数百万节点的图每条信号线都要在有限的布线资源里找到一条合法路径还要满足时序约束还要尽量不造成拥塞这是一个 NP 难问题的近似求解过程复杂度远高于布局。这个统计结果非常关键它告诉我一个方向想压缩 13 小时真正的重点应该放在布线环节而不是综合。很多时候我们把精力花在优化 RTL 让综合快一点方向完全错了。1.2 为什么改两行代码也非要全盘重来这里就要说到 FPGA 工具的一个固有机制了。Vivado 默认每次运行都会把整个工程重新做一遍综合和实现即便是你只改了一行 RTL布局布线器也会把所有逻辑重新放置一遍所有信号重新布一遍线。就好比你写了一篇文章只改了一个错别字却要把整篇文章重新打印一遍之前的排版全部作废效率自然低得离谱。Vivado 不是完全没有优化机制它有个 synth_design 的 cache在 RTL 改动不影响当前模块综合结果的极端情况下可能复用一点中间结果但实际工程里这种机会少得可怜。真正能系统性解决这个问题的是增量编译incremental compile技术这也是我这篇文章的核心内容。2. 增量编译核心思路与适用边界2.1 增量编译到底在“省”什么增量编译的核心思想很简单既然改动的只是工程的一部分那没有改动的部分就不用重新布局布线。具体到 Vivado 的实现上它通过保存上一次实现过程中产生的布局布线结果DCP 文件在新的编译中只对发生变化的逻辑重新做 place 和 route不变的部分直接沿用上次的位置和走线。听起来像是很完美的方案但很多人在实际落地时会发现一个问题直接开 increment 功能后有时候效果并不明显甚至比全量编译还慢。为什么因为增量编译对“哪些地方变了”的识别依赖参照文件而这个识别粒度并没有想象中那么聪明。它只知道某个模块的 RTL 变了就认为这个模块的全部逻辑都需要重做如果这个模块恰好是整个设计里最大的一个那增量编译就退化成半全量编译了省的时间有限。所以想用好增量编译第一步不是打开开关而是让整个设计的结构有利于增量。什么意思呢把大模块拆小、把经常改动的逻辑和基本稳定的逻辑放在不同的层级里这样增量编译才能精准命中“小范围改动”这个前提。这也是为什么黑金、正点原子这些开发板的官方例程很少提增量编译因为它对工程结构有一定要求不是开箱即用的。2.2 增量编译的硬性前提原生时序收敛使用增量编译有一个非常硬性的前提上一次实现必须已经时序收敛。说直白点如果上一次跑完已经有时序违规了这次用增量编译接着改工具会在上次违规的基础上继续优化常常越改越乱最后 timing 一塌糊涂。我项目里第一次用增量编译就吃了这个亏改之前时序本来已经收敛了但有个模块的 hold time 余量只有 0.01ns非常悬。增量编译跑完了那个模块因为没有改动被直接复用老布局结果 find 到老布局里本来就快超标的路径还是继续超标折腾了两天才找到原因。所以增量编译的正确打开方式是先保证当前工程在“干净”状态下能完整收敛然后再开启增量跑后续改动。这条和经验不足的人往往反着来总觉得时序本来就是收敛的直接增量试试看结果出问题也不知道问题出在增量机制上还是自己新写的逻辑上。2.3 增量编译在 xilinx 工具链里的具体长什么样Vivado 从 2013 版本开始就支持增量编译了业界叫 Incremental Implementation核心原理是在写 checkpoints 时额外输出一个参考文件design.dcp下次实现时通过 -incremental 参数指定这个参考文件即可。这部分是整套方案里最基础的一环但没有工程结构配合它就跑不出理想效果。另外还要提一个容易混淆的概念增量综合Incremental Synthesis。Vivado 也有类似机制通过编译缓存保存综合中间结果但实际工程里 RTL 一改综合那块通常也得跟着重新跑增量综合能帮上的忙非常有限主要价值集中在 OOC 模式下的 IP 复用这个后面会单独说。3. Vivado 增量编译实操步骤3.1 准备第一个干净的基线版本拿我的项目举例。为了让增量编译能跑起来我先保证当前工程有一个完整收敛的实现结果。这里强调一下基线版本必须用完整的流程跑一次不能偷工减料因为后续所有增量都在这个版本的基础上展开。具体到 Vivado 的 Tcl 命令先执行综合然后实现synth_design -top top -part xczu9eg-ffvb1156-2-e opt_design place_design phys_opt_design route_design跑完后确认时序收敛WNS 为正HNS 为正然后写 checkpointwrite_checkpoint -force $output_dir/post_route.dcp这个 post_route.dcp 就是后续增量的参考文件里面包含了完整的布局布线结果。为了让后续步骤更可靠还建议在实现完成后立刻保存一份当前的约束文件快照防止之后约束变化导致增量失效。3.2 增量实现时的 Tcl 打开方式现在回头说当时最关键的一步当我后来改了 RTL 两行逻辑需要重新编译时不再从头跑完整实现而是用增量方式open_checkpoint $baseline_dir/post_route.dcp synth_design -top top -part xczu9eg-ffvb1156-2-e -incremental opt_design place_design phys_opt_design route_design write_checkpoint -force $output_dir/post_route_incremental.dcp注意这里有个容易出问题的细节增量实现仍然从综合开始但综合过程会尽量复用 baseline 里未变化模块的逻辑然后布局布线阶段才真正做到“只重新处理变化的部分”。也就是说 synth_design 前的 open_checkpoint 不能省它是告诉工具“我这次要基于上次的结果往下走”。有些教程让人直接跑 implementation省略了这一步结果增量根本不起作用时间一点没省。还有一个更细节的地方synth_design 加 -incremental 时工具会自动寻找工程目录下上次综合生成的网表文件做对比。如果你改了 RTL工具可能会提示 synth 阶段无法完全复用这时不用慌综合全部重新跑也就多一个小时大头在实现实现阶段的增量逻辑仍然能正常触发。3.3 约束文件层面需要处理的一件大事增量编译最怕的不是 RTL 改动而是约束文件的变动。约束一变哪怕只是改了一行 pin 位置都可能让之前所有布局布线结果失去参考价值工具会认为大量逻辑满足不了位置约束迫使它重新放置大量模块增量效果大打折扣。因此我给这个项目做了一件很关键的事给所有约束文件加了版本控制并且严格规定在增量编译模式下不允许直接修改约束文件。确实需要改那就先全量跑一遍新的 baseline再继续增量。你可能会觉得这样太繁琐但实际用下来这反而是省时间最多的一条规矩因为它杜绝了“认为改一点点约束没关系”的侥幸心理避免了很多隐性重跑。还有一类约束需要特别留意物理约束pblock、BEL placement 等。这类约束一旦改动了位置增量编译会直接拒绝沿用旧位置局部重新布局有时候甚至报错提示 0 个 cell 能被增量复用。我在项目里发现凡是手动加过位置约束的模块增量编译的稳定性明显差一些所以能够不加的位置约束尽量不加让工具自由布局。3.4 从 GUI 操作到脚本化这一步很重要很多初学者习惯用 Vivado GUI 界面点按钮。增量编译在 GUI 里也有对应选项在 Implementation Settings 里面有个 Incremental compile 选项勾选后浏览到 baseline 的 dcp 文件即可。但我的一个强烈建议是从一开始就养成用 Tcl 脚本做编译的习惯。GUI 点一次两次没问题但增量编译的调试往往需要反复对比很多次结果。比如你想验证“只改 RTL 不改约束”的增量效果就需要在两种模式下各跑一遍比较耗时和时序。如果每次都在 GUI 里点你连完整参数都看不全更别说批量跑实验了。我自己是把整个流程写成了脚本大概长这样#!/bin/bash # 增量编译脚本 # 用法: ./incr_build.sh baseline_dcp rtl_src_dir output_dir set -e BASELINE$1 RTL_DIR$2 OUT$3 vivado -mode batch -source run_incr.tcl -tclargs $BASELINE $RTL_DIR $OUT配套的 run_incr.tcl 就负责上面那一串 Tcl 命令加上读入源码、设置约束、跑综合和实现。这样每次小改动只需要执行一行 bash 命令编译、报告、DCP 输出全部自动化非常省心。后面我在并行实验不同策略时这套脚本帮了大忙。4. 压缩布线的极限手段多线程和策略调优4.1 多线程布局布线效果立竿见影增量编译能解决“线性等待”的问题但如果你的工程结构不适合增量或者增量之后还有一块很大的模块必须重新布线那布线的绝对耗时还是摆在那里。这时候还有一个简单粗暴的加速手段放开 Vivado 的多线程开关。Vivado 默认布线的线程数非常保守我记得默认值在 1 到 4 之间具体看版本。通过设置set_param general.maxThreads 8可以让布线工具使用更多 CPU 核心并行处理布线区域的划分与探索。这个参数对路由阶段加速非常明显。我自己实测光把线程数从默认 2 提到 8布线时间就下降了接近 35%。但注意多线程不是越高越好超过一定数量后内存带宽会变成瓶颈线程互相抢资源反而拖慢。网上有人在 32 核机器上试 maxThreads 16结果和 8 差别不大内存倒是吃了很多。这里有个实用技巧可以在跑布局布线前先确认机器实际核心数然后设置成物理核心数的一半左右。比如 16 核机器就设 88 核就设 4不要盲目拉满。4.2 布线策略的直接干预除了线程数还有一个我试过很有效的方法就是直接干预布线策略。Vivado 的 route_design 支持多种 directive每个 directive 的核心逻辑和侧重点不一样。默认的 Alternate Routing 比较均衡。到了后期时序紧张时我试过route_design -directive ExploreExplore 策略会让工具尝试多种布线方案理论上会花更久时间但有时反而更快因为它在优化时序的同时可能会找到更高效的路径规划减少反复修正的迭代。但这是个玄学领域具体效果和设计结构强相关必须实测。我个人的建议是不要在生产流程里固定使用 Explore而是把它当做一个“死马当活马医”的策略。真正稳定的加速还是靠增量编译加多线程。如果你每次改动都要全量跑Explore 会让全量时间从 13 小时一步步涨到 18 小时回来后更难收场。4.3 OOC 模式另一种层次的复用除了增量编译还有一个在工程结构上做手脚的办法就是 OOCOut-of-Context模式。简单说它可以把 IP 或者某些模块先单独综合生成独立的 DCP在顶层工程综合时直接引用这个 DCP不用每次重新综合这个模块。OOC 对综合阶段的时间压缩效果确实有尤其在系统里挂了很多复杂的 Xilinx IP 时。比如我的工程里有两个 PCIe IP、一个 DDR4 MIG、一个 Video Processing Subsystem这些 IP 单独综合都很花时间OOC 后它们的综合结果被缓存顶层综合直接把它们当成黑盒子速度能快非常多。但要注意OOC 缓存的是综合结果不是布局布线结果。布局布线层面顶层实现时这些 IP 还是要跟着整体布局布线跑。所以 OOC 对整体编译时间的压缩效果要低于增量实现但胜在实现稳定、对工程结构没有增量编译那么敏感。那么增量编译和 OOC 能同时用吗可以但要注意顺序和缓存一致性。我的经验是OOC 模块的 DCP 版本必须和当前 RTL 一致如果 OOC 对应的 RTL 改了必须先重新综合该模块再进顶层实现否则会用到旧网表。5. 压测结果与常见问题排查5.1 实测效果对比从 13 小时到 5 小时从 13 小时到 5 小时不是某一条优化单独达成的而是好几条路叠加的结果。我整理了一份自己项目的实测表格从原完整流程到最终采用的流程逐步对比方案耗时说明完整流程默认线程约 13 小时原始状态完整流程maxThreads8约 9 小时只加线程布线和布局加速明显增量编译结构未调整约 8 小时部分复用但模块太大复用率有限增量编译 模块拆分 maxThreads8约 6 小时关键改动只影响小模块复用率高增量编译 模块拆分 OOC maxThreads8约 5 小时综合阶段大幅加速实现增量也稳定我最终保留的方案就是表格第三行到第五行之间浮动取决于本次改动的范围。小改动基本稳定在 5 小时左右。这个成绩在工程上算是很理想了。虽然 5 小时也不算短但处理器的世界就是这样能省就省。这里有个重要经验想多说一句加速方案之间不是简单叠加而是先要保证增量编译的复用率高再叠加线程优化顺序反了容易出现线程占用太多导致机器卡死反而把编译拖垮。5.2 常见问题速查表我整理了增量编译过程中最容易遇到的几个问题全是实际踩过的坑可以直接对照排查。问题现象可能原因解决办法增量编译后时序反而更差基线版本本来有时序余量不足先全量收敛再开增量增量编译省时效果微弱改动模块过大/约束变化频繁模块拆小约束冻结提示不能复用任何 celldcp 与当前设计不匹配检查版本、重新生成基线综合阶段 OOC 缓存失效引用了旧网表重建 OOC 运行的输出增加线程后内存爆掉线程数开得太高设置为物理核心一半还有一个容易忽视的点我单独拉出来说一下增量编译完成后一定要对比本次输出 DCP 里的时序报告和基线版本的差异不能只看 WNS 是不是正数。因为增量编译复用了一部分旧布局有时候局部路径的违例不会立刻体现而是在后续多次增量之后积累。我的做法是跑完增量后立即读取时序报告里的所有 endpoints观察有没有新增的 violation有明显异常就回退到基线重新全量跑。这属于少了会省时间但会埋雷的细节。5.3 另一个频繁遇到的大坑IP 版本不匹配增量编译还有一个常见但特别隐蔽的问题当工程里的 IP 被更新到新版本哪怕只是配置参数微调生成的 DCP 文件名可能不变但内部 GUID 变了工具识别到这种变化后会直接放弃复用全量重来。我当时排查了整整一天最后发现是这个原因。解决的方法也很简单IP 升级或参数修改后不要心存侥幸直接删掉相关 OOC 输出和增量参考重新完整跑一次 baseline。这是一个“花小钱避免大坑”的策略。当然做之前最好看清楚版本的变化确认改动是否真会影响实现以免在不对的地方做了妥协。6. 一些其它环节的经验补充6.1 综合阶段的加速技巧综合阶段不像布线那么耗时但它会占掉你约 10% 到 15% 的时间而且增量综合的收益很不稳定。有一个简单有效的针对综合的提速方式设置 synth_design 的 -retiming 参数时谨慎一点。开启 retiming 会大幅提高综合耗时如果你的设计不是必须靠 retiming 来收敛时序建议默认关闭。还有一个关于 RTL 编写习惯的建议层次化设计不要太碎片化。子模块过多会导致综合器反复在层次边界做优化额外增加时间。如果你发现完整的综合时间异常偏长可以检查一下是不是层级切太细了。层次设计是为了可维护性但过度使用会反噬。6.2 内存和 swap 对编译的影响有人把编译慢归咎于电脑配置其实大部分项目瓶颈在 CPU 单核性能和内存而不是硬盘。Vivado 在布局布线阶段非常吃内存一个 7 系列的大工程在布线高峰期占 16GB 内存很常见UltraScale 级别甚至需要 32GB 以上。如果内存不足触发了 swap那速度会跌到让人崩溃这时候不管用什么加速技巧都白搭。我在这个项目里换过一台 64GB 内存的机器虽然时钟频率没有明显提升但因为不再触发 swap布线的稳定性明显好了很多。建议做 FPGA 大型工程的兄弟们内存容量优先级高于 CPU 主频内存不够一切加速都是空谈。6.3 关于 FPGA 开发板、网卡和远程操作的一点个人经验编译 FPGA 往往要跑很长时间很多人选择在公司服务器上远程跑。这里有个非常现实的经验远程跑编译时尽量用 nohup 或者 screen 挂起任务而不是直接在 SSH 窗口里等。因为哪怕你的网络闪断几秒如果任务被中断前面几个小时的编译就全白费了没有任何断点续跑机制。还有一点如果你用的开发板是常见的黑金、正点原子、高云、安路这类国产板卡板卡本身的性能差异对你电脑编译器的时间影响并不大——编译是发生在电脑端的板卡只是最终下载 bit 流用的。不要以为自己花了更多钱买了高端板卡编译速度就会提升不是一回事。这一点经常被新手误解。7. 写在最后的一些建议如果你正在被 FPGA 编译等待时间折磨我建议你先别急着换电脑、换服务器先把增量编译这件事搞明白它应该是投入产出比最高的一步。然后做好约束冻结和模块拆分的工程管理工作再叠加多线程参数优化。这些做完13 小时缩短到 5 小时完全有可能。我自己在写完这篇文章收尾时想到一个有点感慨的事实FPGA 编译等待期其实是一段特别适合看书、写文档、或者处理杂事的时间。以前我嫌等编译耽误时间后来反而学着让这段时间变得有价值——反正跑编译的 CPU 和等结果的人是两个线程互不干扰。现在项目里一般跑编译前我会先花 10 分钟检查一下代码和约束有没有低级错误比如引脚冲突、时钟约束缺失这样每次编译的失败率低了很多反复等待的次数也减少了。毕竟不管编译加速做得多好最省时间的永远是不需要重新编译——一次就把正确的东西写好才是最高级的提速。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进