最近在优化一个基于FPGA的CNN加速器,卷积层的MAC阵列利用率很低,只有不到50%,感觉瓶颈在数据流调度上。面试官问过我这个问题,我当时答得不好。想请教一下,针对卷积层的数据流优化,有哪些常用的策略?比如行固定、列固定、输出固定这些映射方式怎么选?另外,输入特征图和权重的复用怎么设计才能提高MAC利用率?有没有具体的案例分析或优化思路?
2026年了,FPGA工程师做CNN加速器,卷积层计算单元利用率不到50%,面试官问怎么优化数据流和计算阵列映射?
10 个回答

先对齐一下你现在的处境:面试官问的是「数据流和计算阵列映射」,这其实是在考你有没有真正想过 MAC 为什么闲着,而不是背几个名词。你说瓶颈在数据流调度,这个判断方向是对的,但得再往下拆一层——MAC 利用率不到 50%,通常不是阵列本身不行,而是数据喂不上来,或者喂上来了但权重/输入没有在同一拍内对齐到同一个 MAC 上。
我建议你先别急着选行固定还是列固定,先想清楚你的卷积层是什么形状。常见的情况是:输入通道数多、输出通道数也多,但每张特征图的空间尺寸不大,这时候你要是按空间位置去平铺计算阵列,输入行复用率很高,但权重搬运会变成瓶颈;反过来,如果你把权重全部塞进片上,输入特征图又得反复从外部存储器读。所以第一步是统计你的卷积层参数:输入维度 C_in、输出维度 C_out、KK 卷积核、特征图 HW,然后算一下片上 BRAM 能塞下多少权重、多少输入行。
我个人的经验是,行固定(Row Stationary)在大多数场景下是起点,因为它天然把输入行缓存在片上,让同一行输入被多个输出通道的权重复用,这能直接砍掉一部分外部带宽需求。但行固定有个坑:如果你把输出通道维度的并行度开得太大,权重的片上存储会爆炸,导致你不得不每算完一小块就重新加载权重,这时候 MAC 又会闲下来。所以真正的优化是「分块」——把 C_out 切成几个 tile,每个 tile 内部用行固定把输入行复用满,tile 之间用双缓冲把权重预取藏到计算后面。
再往下,脉动阵列不是银弹。FPGA 上的脉动阵列通常要你把权重预先排好,然后数据像流水一样流过去,这个对规则卷积很友好,但一旦遇到 padding 或 stride>1,边界拍数会浪费不少。我见过有人为了脉动而脉动,结果利用率比普通并行 MAC 还低。更实用的做法是先把你的循环嵌套重排一下:最外层是输出通道 tile,中间是输入行分块,最内层让 MAC 阵列同时处理多个输出通道的同一输入行。这样每个输入元素能被 C_out_tile 个权重同时乘,复用率直接翻好几倍。
最后,别忽略工具链。Vivado 的 HLS 或者 Vitis 里有个 report 能看每个循环的 trip count 和 pipeline 间隔,你先把 II(initiation interval)打出来,如果某个循环的 II 大于 1,说明有依赖没解开,比如累加器写后读——解决办法通常是给累加结果做多份拷贝或者用 fabric 寄存器做乒乓。我建议你拿一个具体层(比如 3×3, 64 通道)做实验:先全片上缓存权重,再逐步加大空间并行度,每改一次就记录 DSP 利用率和时钟频率,你会发现有一个拐点,过了那个点利用率上去了但频率掉下来,反而不划算。
你手头现在用的是哪家的器件?如果是 UltraScale+ 的话,DSP48E2 的级联模式可以省不少布线资源,这个对阵列映射影响挺大的。方便的话把卷积层的具体尺寸发一下,我可以帮你算算理论上的最小带宽需求是多少。

面试官问这个,其实想听你怎么做「循环重排」的决策。我自己的套路是先画一张数据流图:每个输入像素会被哪些权重用、每个权重会被哪些输入用,然后看谁在片上待的时间长。行固定和输出固定本质上是两种取舍——行固定牺牲权重存储换输入复用,适合通道数少但空间大的层;输出固定反过来,适合通道数多但特征图小的层。
你利用率不到 50%,我猜大概率是输入特征图从外部读得太频繁。一个很实用的技巧是「输入行缓存 + 权重广播」:把一行输入放进移位寄存器,让它在阵列里横向流动,同时把同一组权重广播给所有列——这样每个输入像素能被所有并行输出通道同时乘,MAC 阵列每个周期都有活干。另外,记得把卷积核的 3×3 展开成 9 个独立的乘加,别在循环里做嵌套累加,那样会引入循环依赖,导致流水线必须停顿。
最后一个小建议:面试时别只讲理论,提一句你用 Vivado 的 HLS 看 II 或者用 RTL 仿真看 stall 周期,会让回答更落地。你要是能说出「我试过把 C_out tile 从 8 调到 16,利用率从 45% 涨到 62%,但 BRAM 用量翻倍了」,面试官基本就认了。你那个加速器是跑在 Zynq 上还是纯 FPGA 逻辑?DDR 带宽不一样,优化重心差挺多的。

先别急着换数据流模板,我建议你把卷积层拆成三个维度去看:输入通道复用、输出通道复用、空间位置复用。面试官问「行固定还是输出固定」其实是在看你有没有意识到,这三种复用不可能同时拿满,你只能根据这一层的形状选一个主复用方向。比如输入通道很深、输出通道也很多但特征图只有 32×32 这种小尺寸,这时候空间复用空间不大,你硬做行固定,输入行缓存还没流几个周期就换下一行了,MAC 当然闲下来。反过来,如果你把权重全部放在片上,输出通道并行度拉满,但输入特征图每次只喂一个像素,那输入带宽又成了墙。我给你的实操建议是:先拿 Vivado HLS 或者 Vitis 的 profile 报告看 DSP 和 BRAM 的占用比,再拿一张表列出你这几层各自的 C_in、C_out、K、H、W,算一下每层的数据量与计算量的比值——这个比值决定了你是「权重受限」还是「输入受限」。如果是权重受限,就走行固定,把一行输入缓存进移位寄存器,横向流过所有 PE,权重广播;如果是输入受限,就走输出固定,把输出通道拆成多个 tile,每个 tile 对应一组权重常驻片上。另外还有个容易忽略的点:卷积核的 3×3 展开成 9 个乘加之后,累加器要单独做流水,别在同一个循环体里做嵌套累加,否则时序收敛后你会发现关键路径全在累加链上,MAC 频率上不去,利用率数字一样难看。最后说个面试话术:别只讲「我选了行固定」,要讲「我看了 profile 发现输入读次数是权重的 3 倍,所以我把行缓存加大到能覆盖 3 行输入,同时让权重在 PE 间广播,这样每个输入像素能被 9 个权重同时消费」。你下次可以试试把你们那层卷积的具体尺寸贴出来,我帮你算算到底该偏向哪种映射。

我觉得你陷入了一个误区:总觉得行固定、输出固定是二选一,其实很多加速器是混合的——空间维度用行固定让输入在 PE 间流动,时间维度上再按输出通道分 tile,权重在 tile 内广播、tile 间换载。你利用率不到 50%,可以先查一下是不是「权重切换太频繁」:比如你每个周期都在从 BRAM 读新权重,但输入像素只被用了一次就丢掉,那等于权重搬运占了大部分时间。一个简单验证方法:把卷积核展开成 9 个独立乘加后,看你的 PE 阵列是不是每个周期都能同时拿到 9 个输入像素和 9 个对应权重。如果拿不到,就说明你的数据流是「按输出像素逐个算」的,这种最伤利用率。改法也直接:把输入行缓存做成双缓冲,预取下一行输入,同时当前行在阵列里横向广播,权重按输出通道分组常驻。如果你能贴出你这层的输入输出通道数和特征图尺寸,我可以帮你判断更偏向哪种方案。

先别急着把所有锅甩给数据流模板,我见过不少项目其实卡在更隐蔽的地方:你的 MAC 阵列每个周期到底有没有拿到「对的输入」和「对的权重」?很多人的 RTL 里循环是展开了,但地址生成逻辑没跟上,导致同一拍内 PE 拿到的输入像素和权重根本不属于同一个输出像素,利用率自然上不去。一个很实用的排查方法:把卷积核展开成 9 个乘加后,用仿真波形盯着某一列 PE,看它连续 10 个周期里有没有 8 个周期在干有效活,如果经常出现「等权重」或「等输入」的空泡,就说明调度没对齐。另外提醒一个容易踩的坑——BRAM 的读写端口冲突。比如你用了双缓冲来预取输入行,但两个缓冲的写端口共用了同一个 BRAM 块,那预取和计算其实还是串行的,等于白做。个人建议你先拿 Vivado 的 Utilization Report 看一眼 DSP 和 BRAM 的比例,如果 BRAM 用了 90% 而 DSP 才 40%,那大概率是数据搬运路径堵死了,这时候与其换数据流,不如先优化存储分块,比如把输入通道切成 4 组,每组对应一小块 BRAM,让权重和输入能在同一拍内被同时读出来。你方便说一下你这层卷积的输入通道和输出通道分别是多少吗?这个数字直接决定你该往行固定还是输出固定那边靠。

顺着你说的「行固定、列固定、输出固定」往下挖一层,你会发现这三个词背后其实是同一个问题:你打算让哪个数据在片上多待一会儿,哪个数据频繁换入换出。行固定(Row Stationary)的核心是让输入特征图的一行在 PE 阵列里横向流动,同一行像素会被多个不同的权重重复使用,这样输入带宽压力小,但代价是权重必须频繁更新——每算完一个输出通道,整组权重就得换一批。反过来,输出固定(Output Stationary)是让每个 PE 负责算一个固定的输出像素,权重在 PE 间广播,输入像素流式进入,这种方案适合输出通道多、特征图小的层,因为权重可以一次性全放片上,但输入特征图得反复从外部读。你利用率不到 50%,我猜大概率是这两种都没选对,而是用了最朴素的「按输出像素逐个算」——每个周期只喂一个输入像素和一个权重,算完一个输出再算下一个,这种数据流在硬件上等于把 MAC 阵列当成了串行乘法器用。我给你的实操建议是:先拿一张纸,列出你这几层各自的 C_in、C_out、K、H、W,然后算一个比值——每层的数据量(C_inKK + C_out)除以计算量(C_inC_outKKHW),这个比值越小,说明计算越密集,越适合用输出固定;比值越大,说明数据搬运越重,越该用行固定。另外可以考虑用脉动阵列(Systolic Array)的思路,让数据像流水线一样在 PE 间传递,而不是每拍都从 BRAM 里重新读——这样虽然单个 PE 的利用率不一定高,但整体吞吐能上去。面试时如果被问到,你可以主动提一句「循环分块(Tiling)的粒度其实比选哪种固定更重要」,因为 tile 大小决定了片上能放下多少权重和输入,这往往比数据流模板更影响实际利用率。

说句直接的,面试官问这个八成是想看你会不会先算账再动手。行固定还是输出固定不是拍脑袋选的,先拿层参数算一下输入复用次数和权重复用次数,哪个复用潜力大就优先保哪个。利用率不到 50%,先查是不是权重每拍都在换,如果是,改成权重常驻、输入流式就完事了。

我觉得先别急着在行固定和输出固定之间二选一,第一步是搞清楚你这层的形状到底适合哪种复用。拿一张纸把C_in、C_out、特征图HxW、卷积核K写出来,算一下每个输入像素会被多少个权重复用、每个权重会被多少个输入复用。如果输入通道很深但空间尺寸小,那行固定的收益就很有限,因为一行还没流几个周期就换下一行了,MAC当然闲。反过来如果输出通道多但输入特征图大,输出固定又会让输入带宽爆掉。个人经验是,先拿一个层做实验,把数据流模板从行固定换成输出固定,对比一下DSP utilization,往往能差出20个百分点。面试时能说出这个判断过程,比背名词强多了。你方便说下你那层大概的通道数和特征图尺寸吗?

补一个很多人忽略的角度:MAC利用率低,有时候不是数据流模板选错,而是你的PE阵列规模跟卷积层的并行度不匹配。比如你设计了一个8×8的PE阵列,但这一层输出通道只有16个,空间维度又是奇数尺寸,那映射完肯定有空闲的PE。常见做法是先按输出通道数把阵列宽度定下来,再按空间位置把阵列高度定下来,别拿一个通用阵列硬套所有层。另一个容易踩的坑是权重更新节奏:如果你每个周期都从BRAM读新权重,但输入像素只被用一次就丢,那等于权重搬运占了大半时间。一个很实用的改法是输入行缓存做成双缓冲,预取下一行输入的同时让当前行在阵列里横向广播,权重按输出通道分组常驻片上,这样每个MAC每拍都能拿到对的输入和权重。你可以先看下Vivado的Utilization Report,如果DSP利用率高但BRAM读写冲突多,大概率是调度没对齐而不是模板选错。你用的是HLS还是手写RTL?这个对排查路径影响挺大。

这个问题我建议你从「到底是谁在等谁」这个角度去拆。MAC利用率不到50%,本质上是每个周期里,要么输入没到位,要么权重没到位,要么两者都到了但配错了对。很多人的第一反应是换数据流模板,但实际项目里我见过更多是循环展开和地址生成没配合好——RTL里循环是展开了,但同一拍内PE拿到的输入像素和权重不属于同一个输出像素,那MAC就算在跑也是白算。你可以在仿真里盯着某一列PE看连续10个周期,如果出现等输入或等权重的空泡,就说明调度没对齐,这时候改数据流没用,得先修地址生成逻辑。
如果确认调度没问题,再谈行固定和输出固定的取舍。行固定的核心是让输入特征图的一行在PE阵列里横向流动,同一行像素被多个权重重复使用,这样输入带宽压力小,但代价是权重必须频繁更新——每算完一个输出通道,整组权重就得换一批。输出固定反过来,每个PE负责一个固定的输出像素,权重广播、输入流式,适合输出通道多但特征图小的层。但注意,这两种都不是银弹,很多加速器实际用的是混合策略:空间维度做行固定让输入复用,时间维度按输出通道分tile,权重在tile内广播、tile间换载。这样比纯行固定少了一半权重搬运,比纯输出固定少了一半输入带宽。
最后给你一个可落地的排查路径:第一步拿仿真波形看PE空泡,第二步看BRAM读写冲突,第三步才谈模板选择。如果前两步没问题,第三步通常能解决一半以上的利用率损失。面试时能把这个排查顺序讲清楚,面试官会觉得你有实战 sense。对了,你现在的阵列是几乘几?输出通道是按多少路并行展开的?这两个参数决定了你能用的分块策略。
发表回答
登录后可在本页底部提交回答