尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA编译加速实战:从13小时到5小时的优化路径与工程实践

FPGA编译加速实战:从13小时到5小时的优化路径与工程实践 做大型FPGA工程的人对“编译加速”这四个字的体会绝对不止是点一下Progress看进度条那么轻松。我前阵子处理一个包含了PCIe、DDR、MIPI图像采集、Biss-C编码器解析、还有卡尔曼滤波的综合项目Vivado全流程跑下来13小时属于典型的“下午启动、第二天早上看结果结果还经常死在布线那一步”的状态。后来我把流程从头到尾重新梳理了一遍项目RTL基本没动只是把机器环境、工具参数、增量复用和约束里的隐形问题处理干净同样的设计压缩到了5小时以内。这篇文章不聊那些玄乎的“优化框架”只讲我在这个工程上真正做过、并且验证有效的事。1. 13小时的时间黑洞先摸清编译流程各阶段的花费1.1 综合、优化、布局、布线谁在偷偷吃掉时间拿到一个13小时的工程第一件事不是去调工具参数而是先搞清楚时间到底烧在哪个阶段。Vivado的GUI里有Report Runtime跑完一次完整流程后能直接看到每个步骤的耗时如果是Tcl流程也可以用time命令包一层。我这边基线数据大致是这样阶段基线耗时占比主要工作综合 synth_design2h00m约15%RTL转网表受逻辑规模和层次影响逻辑优化 布局 place_design4h30m约35%常量传播、寄存器合并、cell摆放和拥塞控制布线 route_design5h00m约38%给所有net找物理通路处理绕线和DRCbitgen 时序分析等1h30m约12%生成比特流、跑时序报告布线是绝对的大头。很多人第一次接触FPGA实现流程时觉得布线不就是把所有连线拉通吗为什么这么慢实际上布线器面对的是几千上万个pin、几十万条net在满足工艺规则和拥塞约束的前提下找可行解本质是个离散优化问题。它不像人手工画PCB那样可以盯着局部慢慢调它要全局统筹改一条net可能影响周围一片资源。这就导致布局阶段留下的任何隐患到布线阶段都会被放大。还有一点容易被忽略综合阶段之所以耗时不小是默认情况下很多IP和子模块都被重新综合了一遍。如果工程里挂了MIG、PCIe、MIPI D-PHY这类大IP光这些IP的综合就能占用半小时以上。后面我会详细讲怎么把这些“重复劳动”彻底减掉。1.2 同样的工程换台机器为什么能差两三倍这里先泼一盆冷水如果你的工程跑在NFS网络存储上就算把CPU从16核换成64核你可能也感受不到成倍的提升。我基线那13小时就是跑在公司服务器上的双路24核配置不算差但工程目录整个放在NFS上。Vivado跑实现时会在.runs目录里产生大量小文件每一次读写都要经过网络文件系统连文件锁都要走网络round trip这一项白白吃掉的时间非常可观。第二个隐形损耗是工具默认线程数。Vivado里general.maxThreads的默认值通常是4很多服务器明明空闲着一大半核心工具却只用4个线程在跑。这不算bug是EDA工具一贯保守的行为——并行会改变布局布线结果工具厂商宁可在默认参数上求稳。但对我们来说这就是白白浪费的算力。第三个坑是内存。Place和Route阶段的内存占用跟设计规模和资源利用率强相关高利用率的大工程跑到一半吃掉几十GB内存很正常。如果机器只有16GB或32GB系统开始换页到磁盘那4小时的布线能拖到10小时以上。看任务管理器里内存是不是红了往往比调工具参数更先解决问题。2. 把闲置的CPU利用起来并行线程与多Job调参2.1 Vivado里最常见的三个并行入口在我这个工程里验证后实际有效的是三个地方第一个是全局线程数。Tcl脚本最前面加一行set_param general.maxThreads 8如果你的服务器是16物理核设置8是安全线如果是32物理核可以试12到16。我最终在这台双路24物理核服务器上用的是8再往上跑WNS开始出现波动收益也不明显了。第二个是综合和实现阶段的-jobs参数。Vivado 2019.2之后的版本synth_design和impl_design都支持-jobs N它能让工具在可拆分的子模块上做并行处理。注意一个前提综合时不要用完全打平层次的策略层次保留得越完整并行粒度越大收益越明显。Fully flat的网表基本没有可并行的边界。第三个是launch_runs的-jobs。整条命令可以这么写open_project /home/build/fpga_accel.xpr set_property strategy Flow_RuntimeOptimized [get_runs impl_1] launch_runs impl_1 -jobs 4 -to_step write_bitstream wait_on_run impl_1 open_run impl_1 report_timing_summary -file timing_summary.rpt report_utilization -file utilization.rpt report_congestion -file congestion.rpt这一段可以直接存成build.tcl以后每次编译都通过vivado -mode batch -source build.tcl执行。把日常构建脚本化之后不仅省掉了开GUI等待的时间更重要的是每个工程师拿到的流程都一样不会出现“你机器上参数和我机器上不一样”的扯皮。2.2 Quartus与国产工具链的等效设置用Quartus做Intel平台的朋友也不用羡慕对应的设置同样存在。GUI里在Assignments - Device - Device and Pin Options - Compilation Process settings里可以设置并行编译进程数对应的QSF约束是set_global_assignment -name NUM_PARALLEL_PROCESSORS 4要在命令行模式跑的话quartus_map和quartus_fit都支持--parallelN参数quartus_map --parallel4 top_level quartus_fit --parallel4 top_levelQuartus还有一个“Smart Recompile”特性也就是常说的增量编译它和Vivado的增量实现思路类似前提是改动足够小。老版本叫Smart Recompile新版Prime里在Compilation Process相关设置里也能找到。至于高云、安路这类国产工具链我最近也在一两个项目里接触过。它们的编译本身比Xilinx小工程快不少但并行度和增量编译的支持还在成长期能调的旋钮不多。遇到这种环境我的建议是不要跟工具死磕并行把精力放在机器本地磁盘、内存容量和约束正确性上收益反而更直接。2.3 并行的边界不是核心越多越快并行布局布线并不是把同一份工作拆给4个人做得更快这么简单它更像让4组人同时设计一栋楼的不同楼层每层内部可以快速推进但楼层之间的管线、承重结构怎么搭还是需要整体协调。线程越多协调成本越高结果的不确定性也越大。我实测下来几个规律在同一个工程上maxThreads从默认4提到8全流程能明显缩短从8提到16收益曲线变缓从16提到32不仅没快WNS还出现了几十皮秒的劣化。原因很简单物理核就那么多超线程带来的逻辑核做布局布线这种重计算收益有限反而增加了内存带宽竞争。另外要注意内存。每个并行线程都会增加内存压力跑之前先用free -g看一下机器内存余量。如果在布线阶段发现swap开始增长不要犹豫立刻把线程数降下来。线程多但内存在换页这个状态比少几个线程更致命。跑的过程中可以用htop观察CPU利用率长期在90%以上且稳定说明并行是有效的如果看到某个进程在D状态等IO问题多半出在磁盘或网络存储上。3. 增量编译与DCP复用让工具只处理真正变化的部分3.1 增量编译为什么能快又为什么不是万能药增量编译的原理不复杂工具把上一次综合或实现的中间结果当作参考这一次只处理发生变化的部分其余直接复用。我打个比方它更像编辑器的“增量保存”而不是每次CtrlS都把整篇几千行的文档重新写入硬盘。在Vivado里综合侧可以用synth_design -incremental指定上一次综合生成的参考DCP实现侧的思路类似跑一次完整流程后保留route_design的DCP作为参考下次小改动就在这个参考上做增量place和route。不同版本的具体开关名有点差异建议以装的那个Vivado版本配套的UG904为准但整体思路是一样的。增量不是万能的。它只适合“小改动”场景比如改了一两行RTL、调了一个参数、加了一条约束。如果版图级结构发生了剧烈变化比如顶层模块换了个连接方式、某个大模块整体重写增量参考反而会成为负担工具要带着旧约束重新协调还不如直接全量跑。更关键的一点是增量编译跑得快不代表结果质量好。工具复用了旧布局布线的框架如果新逻辑恰好落在原来拥塞的区域局部绕线会比全新布局更差。所以我给自己定了个规矩每次增量跑完必须对比report_timing_summary里的WNS和TNS劣化超过一定范围就回退到全量。3.2 OOC与IP综合缓存最容易忽略的“现成资产”OOC也就是Out-Of-Context脱胎于“孤立上下文综合”。Vivado的IP Catalog默认对IP做OOC综合每个IP在自己的独立synth run里先综合成网表并缓存下来。这样做的最大好处是只要IP配置没变下次全流程构建可以直接复用这个网表不用在顶层综合时把所有IP再推一遍。我这里遇到的情况很典型工程里的MIG DDR控制器、PCIe核、MIPI D-PHY三个大IP的综合加起来接近40分钟。一开始没人动OOC设置每次全量构建都在重复烧这40分钟。后来确认这几个IP的配置不再变化直接把它们的综合结果固化成DCP留在工程目录里。之后即使改动顶层逻辑只要不碰IP配置综合阶段就能稳定在一小时以内。这里有个容易踩的坑很多人为了省磁盘清理工程时会把.runs目录整个删掉。这在Vivado里非常伤因为IP的综合缓存、甚至某些中间checkpoint都在这下面删了之后下次构建全部从头算等于把前面省的时间全吐回去。我的建议是.runs目录里认准IP对应的_synth_1子目录这个不要轻易动真要清磁盘优先清历史备份和日志。3.3 大工程里做参考Checkpoint的实操流程具体到我这边的做法可以归纳成四步花一次完整构建的时间跑出一个质量合格的基准版本确认WNS为正、没有DRC error。把这个版本的route_design DCP单独备份出来放到固定目录命名里带上日期和构建tag比如route_ref_20250117_1200.dcp。后续小改动就用这个DCP作为实现侧参考配合-incremental跑。每攒到一定程度比如改动了较大的模块或者更新了Vivado版本重新生成一次基准DCP扔掉旧的。这套流程听着简单但能把“增量”从碰运气变成可控操作。最关键的就是固定目录和命名规范。流程脚本化之后人不用记今天该用哪份DCP脚本从约定好的路径读就行了出错概率直线下降。4. 从13小时到5小时的优化实录4.1 基线工程PCIe、DDR、MIPI和图像处理堆出来的13h先交代一下工程背景。用的是Xilinx Kintex UltraScale XCKU040资源利用率不算低LUT用了约61%BRAM 70%DSP 49%。时钟一共9个最紧的是PCIe参考200MHz、MIPI像素时钟150MHz和DDR 300MHz这几组。模块方面有PCIe DMA通道做数据上传DDR3通过MIG做缓存前面是MIPI D-PHY接收图像中间是一整条图像处理流水线包括滤波和几处乘加运算另外还有一个跟STM32H743通过FMC通信的控制口。Biss-C编码器走的是低压差分信号采样逻辑和跨时钟处理也在这颗FPGA里。这样的设计模块之间跨时钟路径非常多只要约束有一点不干净布线器就要花大量时间去优化那些本不该被优化的路径。基线13小时就是这么来的NFS存储、默认4线程、策略还是Default、IP每次都是重新综合、还有一个历史遗留的Pblock箍住了图像处理模块。四个问题叠加哪有跑得快的道理。4.2 第一轮调整线程、策略和运行环境第一轮我没有改任何RTL和约束只动了三件事把工程从NFS迁移到服务器本地NVMe盘上剩余空间预留了200GB以上。在Tcl脚本开头加上set_param general.maxThreads 8。把实现策略从默认切到Flow_RuntimeOptimized布局布线指令换成place_design -directive RuntimeOptimized和route_design -directive RuntimeOptimized。一轮跑完全流程从13小时降到8小时40分。WNS从0.21ns降到0.15ns左右还都是正余量TNS为0没有出现时序违例。这个结果说明工程原来的时间大量浪费在环境层面跟设计本身关系反而不大。当时有个小细节迁移到NVMe之后第一件事不是跑全流程而是跑了一次增量验证。之前NFS上卡IO的情况立刻消失综合阶段快了20分钟。IO瓶颈对EDA的影响比很多人想象中大得多。4.3 第二轮调整IP缓存与增量实现第二轮开始动流程结构。我把MIG、PCIe、MIPI D-PHY三个大IP的OOC综合缓存确认了一遍不再让顶层run重复综合它们。然后对改动比较小的迭代直接用上一版route_design.dcp做增量实现不再每次从头place和route。这一轮效果立竿见影综合从1小时30分缩短到1小时以内布线在增量模式下从3小时30分压到1小时45分全流程到5小时5分。WNS继续往下走了一点到0.11ns附近余量开始有点紧张但时序报告还是干净的。这里我多说一句增量实现在这个阶段能省这么多是因为当时的代码改动集中在图像处理流水线里的一小块顶层框和DDR、PCIe这些大块都没动。如果你今天改了顶层连接关系明天又换了接口协议增量能帮你的就非常有限了。所以增量该不该用第一判断标准是“我到底改了多少、改在哪个层次”。4.4 第三轮调整Pblock和约束里的隐性炸弹时间压到5小时出头之后我其实已经满意了但接下来碰到一次布线爆慢逼着我把最后的问题挖了出来。那次只改了一处RTL逻辑增量跑却花了两倍时间。查report_congestion发现DSP密集的图像处理区域有大量高拥塞net绕行追根溯源是前人留下的一小块Pblock——它圈定的区域资源太紧导致布局器把大量cell塞进临近区域布线器只能在外围绕长线。我把那个Pblock删掉让Vivado自己floorplan同样设计下布线时间直接砍下来40分钟WNS还从0.11ns回弹到0.18ns。与此同时我在report_clock_interaction里看到PCIe参考时钟和MIPI像素时钟之间有大量跨域路径分析。这两组时钟在实际设计中是不需要相互约束的但工程里一直没有写set_clock_groups工具不得不把它们当作同步路径处理白花了很多优化时间。补上约束之后set_clock_groups -asynchronous \ -group [get_clocks -include_generated_clocks pcie_ref_200m] \ -group [get_clocks -include_generated_clocks mipi_pixel_150m] \ -group [get_clocks -include_generated_clocks ddr_clk_300m]布线阶段又缩短了差不多半小时。这一轮做下来调试用的快速流程稳定在4小时30分WNS 0.117ns虽然余量不算宽裕但作为日常迭代足够用。如果要出发布版本我会回到Default策略配合增量复用全流程大约5小时10分WNS回到0.185ns。4.5 优化前后的对比表与可复用的核对清单整条优化路径的时间变化如下轮次综合布局布线bitgen等全流程WNS基线NFS/默认参数2h00m4h30m5h00m1h30m13h00m0.210ns第1轮本地NVMe 线程1h30m2h30m3h30m1h10m8h40m0.152ns第2轮IP缓存 增量实现1h00m1h30m1h45m0h50m5h05m0.116ns第3轮修Pblock 洗约束0h50m1h00m1h00m0h40m4h30m0.117ns发布构建Default 复用0h50m1h20m1h50m1h10m5h10m0.185ns以后不管谁接手这个工程我都会甩给他一份核对清单工程目录必须放本地NVMe或SSD不允许在NFS上跑全流程。编译前确认物理内存剩余充足free -g看一遍。set_param general.maxThreads要和机器物理核匹配别无脑拉高。大IP用OOC.runs目录别乱删。增量跑之前确认改动范围小跑完必查WNS/TNS。约束里该写的set_clock_groups和set_false_path一个都不要省。5. 提速之后必须做的代价审计与质量兜底5.1 并行和RuntimeOptimized对时序余量的真实影响提速这件事没有免费的午餐。RuntimeOptimized之所以快是因为它减少了布局布线的迭代次数用了更激进的启发式策略。代价就是最终结果和Default策略会有差异最直观的就是WNS变化。我在这个工程上实测的结果是同样一套RTL和约束Default跑出来的WNS是0.21nsRuntimeOptimized只有0.12ns左右余量直接少了近一半。如果你的设计本身就在时序收敛边缘挣扎WNS常年是0.05ns、0.03ns这种水平千万别用RuntimeOptimized。这种快速策略适合“我还有余量、我需要快速验证功能”的场景比如调DDR读写逻辑、改图像算法中间某级流水这时候编译越快越好布局质量差一点无所谓。到了发布阶段该回到Default还是回到Default。5小时10分的发布构建对我这个工程完全可接受没必要为了再省半小时拿一个余量很薄的bitstream去流片或交付。5.2 一键可查的报告WNS/TNS、拥塞度和DRC每次跑完编译特别是用了增量或者RuntimeOptimized之后至少要看这几个报告report_timing_summary看WNS、TNS、WHNS这是时序质量的直接体现。report_congestion如果布线阶段耗时异常先看拥塞图通常能直接定位到是哪块区域在“堵车”。report_utilization确认资源利用率没有因为并行策略出现离谱的变化。report_clock_interaction看跨时钟域路径是不是符合业务预期。report_route_status看有没有undriven pin、unrouted net之类的低级问题。我一般会把这几个报告的文本导出和前一版做diff。WNS掉了超过50ps或者TNS从0变成了负数就得停下来追原因而不是继续叠加新的优化手段。很多“编译越跑越慢”的坑其实都是在快速流程里埋下质量隐患后面又花几轮时间去还债。5.3 什么时候绝不能用快速策略总结几条我的硬性规则WNS已经是负值的时候不要用RuntimeOptimized也不要盲目叠加增量。最终发布、交付客户、或者做板级时序验收的时候必须用Default或Explore策略重新跑一遍完整实现。刚升级Vivado版本后的第一次构建不要做任何增量先全量跑出新的基准。如果团队里有多个工程师共用一台编译服务器并行线程数和并发构建数要约定好否则互相抢CPU大家一起变慢。这些规则看着保守但能避免大多数“优化到最后反而翻车”的情况。6. 从设计源头把“编译慢”彻底降下来6.1 RTL层的组合逻辑深度才是原罪工具层面的优化再多也解决不了RTL本身写得让布局布线很难受的问题。组合逻辑深度过深是编译慢最典型的“设计债”。比如我这套图像处理流水线里有一处卡尔曼滤波相关的计算几个乘加串在一起组合逻辑链能到八层以上。跑到150MHz这种频率下setup就很难收布局布线器为了压缩这条关键路径会反复尝试不同的cell摆放和绕线一个模块拖累整个工程。后来我给中间结果加了寄存器把一条长链拆成两三级流水。时序好收不说布线时间也跟着降——因为工具要优化的“关键路径”数量变少了它自然跑得更快。这里有个反常识的点多刷几个FF并不会增加多少编译负担相反组合逻辑层级降下来之后布线器的压力会下降一大截。不要为了省几个触发器让工具在布局布线阶段多跑几个小时。6.2 约束写对了才能真正减少迭代轮数约束问题对编译时间的影响很多人排到最后才想得到实际上它经常是最大的隐性杀手。举个最常见的场景两个异步时钟域之间本来不需要时序收敛但你没有写set_clock_groups或set_false_path。工具不知道这层业务关系就会把跨域路径全部纳入时序分析然后花大量时间去优化一条本来不该被优化的路。跑出来的报告里还会出现一堆假violation逼着你一遍遍“修”每修一次就是一轮全量编译时间就是这么螺旋消耗掉的。约束的正确用法不是给工具加压而是帮工具缩小优化空间。时钟关系写得越准确工具越知道哪些路径必须死磕、哪些可以放弃布局布线器的精力才能花在刀刃上。这个理解到位之后你会发现很多“编译慢”的工程其实是“约束不准确”的工程。6.3 团队流程里的编译资产复用思路最后说一点超出单机范围的思考。编译加速不只是个人的事团队协作方式对编译时间的影响可能更大。我现在的建议是把编译做成自动化流水线RTL代码合并后自动触发一次完整构建把综合结果、实现报告、DCP全部归档。第二天谁需要看时序直接拉报告不用每个人都在自己机器上重跑一遍。IP的OOC缓存更要当成团队资产来管谁改了IP配置要通过变更记录明确标示没改的模块大家共用同一份缓存。对于Zynq和MPSoC这类带PS端的项目还有一条很实在的经验PS端的软件改动不要每次都触发整个PL重新编译。PL侧代码没动硬件设计没变固件更新就不需要重新跑一遍Bitstream流程。把这些不同环节的构建关系从流程上分开团队的整体编译等待时间能降好几个量级。更进一步如果某个功能模块是独立的、可重构的可以研究静态部分重构。把固定逻辑和可变逻辑拆开调试时只编译变化的那个分区的网表和比特流其余部分原样复用这种架构级的加速比任何工具参数都彻底。当然这个方案对设计架构有要求不是所有工程都能用但一旦用上你会觉得“等13小时”这个词彻底成为历史。我在实际做这个优化项目之前一直以为编译加速是IT运维或者EDA工具专家的事。真跑完这一轮之后才发现里面大部分动作比如看时间分布、调线程数、管理DCP缓存、清理约束、排查Pblock都是我们普通逻辑工程师完全能自己动手做的事。下次再有人跟你说FPGA工程跑得慢只能换服务器你可以先打开Report Runtime看一眼再决定要不要信他。
返回列表