最近在做一块基于国产FPGA的AI推理加速卡,用HLS写了一个3×3卷积核,C仿真和C/RTL协同仿真都通过了,但综合后频率只能跑到150MHz,离目标200MHz差得远,而且上板之后数据偶尔出错。时序报告里显示关键路径在DSP48的输入寄存器和后续加法树之间。想问问有经验的大佬,HLS代码里哪些写法最容易导致这种问题?比如循环展开、数组partition、pipeline间隔这些应该怎么调?
2026年了,FPGA工程师做AI加速卡,用HLS写的卷积核综合后频率上不去,C/RTL协同仿真通过但上板时序乱掉,问题出在哪?
5 个回答

先别急着改代码,把时序报告里那条关键路径展开看看,到底是DSP48输入寄存器到加法树之间插了几级组合逻辑。HLS最常见的坑是C代码里写了个累加循环,工具默认给每个乘法结果直接相加,中间不插寄存器,这就会让加法树深度变成log2(N)级,频率自然上不去。你试试在累加循环外面手动拆成两段,或者用#pragma HLS latency给加法树留一拍,让工具知道这里可以插寄存器。另外检查一下卷积核的输入数组有没有做partition,如果整个图像缓冲被当成一个大数组,BRAM只有一个读端口,每次取3×3窗口要串行读9次,pipeline根本推不起来。先做array_partition把行缓冲拆开,再看II能不能压到1。你用的国产FPGA是哪个系列的?有些厂家的DSP48布局和Xilinx不一样,寄存器打拍位置可能要手动调。

150到200这个差距,大概率不是频率本身的问题,而是你给HLS的约束太宽松了。C/RTL协同仿真能过只能说明功能对,不代表综合后时序能收敛。我建议你先跑一下HLS自带的performance analysis,它会告诉你每个循环的II和Trip Count,如果卷积核的循环嵌套里内层循环的II是3或者4,那问题就出在数组访存冲突上。3×3卷积至少要读9个像素,如果输入图像存在BRAM里且没做partition,单端口BRAM一个周期只能读一个数,II=1就是不可能的任务。解决办法是把输入行缓冲拆成三份,每个行缓冲独立一个BRAM,这样三个像素可以并行读,配合pipeline II=1。还有个容易忽略的点:DSP48的输入寄存器。HLS默认不会自动给乘法器输入打拍,你得在乘法操作前显式定义中间变量,或者用#pragma HLS resource指定DSP48并勾选input register选项。上板数据偶尔出错,我怀疑是复位时序问题,HLS生成的IP核有些信号需要固定时序关系,你检查一下ap_rst和ap_start的握手时序,尤其是跨时钟域的部分。另外国产FPGA的工具链对HLS的支持一般不如Vivado成熟,建议你看看综合报告里DSP48的使用率,如果资源利用率超过80%,布局布线阶段频率会掉得厉害,适当降低并行度反而能跑更高。

说个可能被忽略的点:你用的是国产FPGA,它的DSP48结构可能和Xilinx的DSP48E2不一样。HLS工具是通用的,它生成的RTL是按照Xilinx的DSP48特性优化的,比如输入寄存器和级联路径,国产器件如果没有对应的原语映射,工具会退化成用LUT实现乘法,那频率当然上不去。你先查一下综合报告里DSP48的使用情况,如果显示DSP48E2但你的器件是国产的,那说明工具在兼容模式下工作,性能会打折。解决办法是看看国产FPGA的HLS工具链有没有提供专门的DSP原语库,或者你在C代码里用#pragma HLS bind_storage指定特定的资源类型。另外,时序报告里关键路径在DSP48和加法树之间,说明乘法器本身没问题,问题在数据从DSP48出来到加法树的路径上。你可以试试在C代码里把乘法结果先存到一个局部变量,再参与累加,这样工具会在这个变量处插入寄存器。上板数据出错,我猜是复位的问题。HLS生成的模块通常有ap_rst信号,如果你的系统里复位是异步的,但HLS默认生成同步复位逻辑,时序上可能不匹配。你检查一下复位信号是不是在时钟上升沿附近变化,如果是,改成异步复位或者加个同步器。最后,你提到C/RTL协同仿真通过,但上板出错,这个现象很典型——协同仿真用的是理想时序,没有考虑时钟偏斜和布线延迟。你可以试试在协同仿真里加上时序约束,或者用Vivado的时序仿真替代C/RTL仿真,那个更接近实际。你现在用的HLS版本是哪个?国产FPGA厂商一般会基于某个开源的HLS工具做二次开发,版本旧的话很多优化指令不支持,建议升级到最新版。

我把话往深处说一点。你现在的症状是C/RTL协同仿真过了但上板时序乱,这类问题十有八九是HLS给你生成的RTL和你心里想的RTL不是一回事。HLS的本质是调度器,它把C代码翻译成数据流图,然后按照你给的约束去安排每个操作发生在哪个时钟周期。你写一个for循环累加9个乘法结果,工具默认认为这个加法链可以在一拍内完成,因为它不知道FPGA上LUT的延迟是多少。实际上9个32位加法串在一起,LUT级数可能超过15级,在200MHz目标下根本没法收敛。这就是为什么时序报告会指向DSP48输入寄存器和加法树之间——工具把乘法放在DSP48里,但累加器是用LUT搭的,两者之间的路径没被约束住。解决思路不是去调某个pragma,而是理解HLS的流水线寄存器插入规则。你可以在累加循环里手动写一个中间变量,比如每三个乘法结果先加一次,再把三个中间值相加,这样工具就知道这里有数据依赖边界,会自动插入寄存器。另一个更系统的做法是改用行缓冲+滑动窗口的结构,把卷积核的权重提前加载到寄存器数组里,输入像素按行流式进入,这样数据通路是规则的,HLS调度器更容易优化。我见过有人用HLS写卷积核,最后发现瓶颈在输入图像的DMA搬运上,HLS生成的接口是AXI-Stream,但你的DMA配置成burst模式,每次搬运的beat数不够,导致数据断流,上板就会偶发错误。你最好在ILA里抓一下AXI总线的握手信号,看看是不是有ready拉低的情况。最后说一句,HLS做AI加速卡不是不能做,但你要接受一个现实:HLS擅长控制逻辑密集的算法,比如视频缩放、滤波,但对于卷积这种访存密集型的算子,手写RTL的时序可控性会好很多。你如果只是想把板子调通,先按我说的把累加链拆开,array_partition做上,估计能到180左右;想稳上200,建议考虑把卷积核单独拎出来用RTL写,HLS只做数据调度。你现在的工程是纯HLS还是混合了RTL模块?混合的话可以试试在HLS里用import logic把卷积核替换掉。

我看了一圈,没人提上板数据出错这个关键点。频率差50MHz只是性能问题,但数据偶尔错是功能问题,这两件事要分开查。你先确认一下C/RTL协同仿真是不是覆盖了边界情况,比如图像边缘的padding处理,还有卷积核权重是不是有符号数。HLS默认int类型是32位有符号,如果你在C里用了unsigned char存像素,但乘法结果赋值给int,符号扩展可能会出问题。上板出错更常见的原因是跨时钟域,你的加速卡上肯定有DDR或者PCIe,HLS生成的模块时钟和总线时钟如果不是同源,中间一定要经过异步FIFO或者双口RAM缓冲。你检查一下HLS接口配置,如果用了ap_memory接口去读DDR,但没有配置latency和burst属性,工具会默认生成简单的读写时序,实际DDR延迟一变化就出错。我建议你先用ILA抓一下出错时刻的数据,看看是输入数据错还是输出错,如果是输出错,大概率是累加链的中间值溢出,3×3卷积的乘加结果范围是输入位宽加9位,你预留够了吗?至于频率,150到200其实不算大差距,你试试把DSP48的输入寄存器用resource pragma强制打拍,同时把加法树用#pragma HLS expression_balance打开,这个选项会自动平衡组合逻辑深度。还有个小技巧,把卷积核的循环顺序从行列改成一维展开,让HLS更容易做循环并行。你现在的循环结构是嵌套的还是展开的?展开程度越高,DSP48和LUT占用越多,但频率不一定更高,有时候资源占用率超过70%布线就会变差。建议你先用默认设置跑一次,看资源报告,再逐步加pragma,每次只改一个变量,对比频率变化。
发表回答
登录后可在本页底部提交回答