最近面了几家做AI芯片的公司,都被问到同一个问题:用FPGA实现CNN加速器时,MAC阵列利用率总是上不去,比如卷积层实际有效计算只有60%左右。我查了资料说跟数据流调度有关,有行静态、列静态、输出静态这些讲究,但真到自己写RTL就不知道怎么选。想问问有经验的前辈,不同卷积层和池化层组合下,到底怎么设计数据流才能把MAC利用率拉高到90%以上?面试官问这个一般想听什么层面的回答?
2026年了,FPGA工程师用Verilog写CNN加速器,MAC阵列利用率上不去,面试官问怎么优化数据流?
11 个回答

先说结论:面试官问这个,多半不是要你背数据流名词,而是想听你怎么在面积、带宽和延迟之间做取舍。你提到的行静态(WS)、输出静态(OS)本质上是把卷积循环的乘法顺序重排,让某个维度的数据尽量留在片上。举个例子,3×3卷积、输入通道32、输出通道32,如果按OS流,每个输出像素要累加288次乘法,这时你得把输入行缓存(Line Buffer)做成3行x32通道的深度,每次来一个新行就滑动更新,同时把权重广播到所有PE;而WS流则是权重不动、输入行流式灌入,适合大卷积核但输出通道少的层。实际项目里很少纯用一种,常见做法是:第一层卷积用OS(因为输入是大图、通道少,行缓存便宜),中间层如果输入特征图变小了、通道变多,就切到WS,让权重留在片上、输入行从DDR流式读。MAC利用率上不去,很多时候不是数据流选错,而是你没有把空闲周期藏起来——比如池化层和ReLU占用的周期没和卷积重叠,或者最后一个输出通道的累加器在等前面通道的偏置。我见过一个优化技巧:把相邻两行输入合并成一个向量,用两个MAC同时算两个输出通道,这样Line Buffer的读带宽能省一半,代价是权重寄存器多一倍。面试时你如果能画出数据流图,标清每个周期每个PE在算什么、数据从哪来,就已经超过大多数只会背「OS比WS好」的人了。追问一句:你之前那60%利用率,是单层测的还是整网端到端测的?单层的话先查行缓存深度是不是只够一个通道,端到端的话瓶颈多半在DDR带宽而不是MAC。

个人感觉你被绕进「数据流」这个词了。面试官真正想看的是你有没有算过一笔账:一个MAC周期要喂进去两个操作数,一个来自Line Buffer,一个来自权重寄存器堆,你片上RAM的读端口够不够?如果每周期只能读一个数,那MAC阵列再大也是空转。我建议你从最简单的OS流开始写RTL,先把一个3×3卷积层调到90%以上,再谈多通道并行。具体做法:输入行缓存做成双缓冲,当前行组在算,下一行组在预取;权重全部放寄存器,别用BRAM,因为广播权重每周期都要读。等你把单层调顺了,再去处理池化层和卷积层之间的数据拼接,那才是真正掉利用率的地方。别一上来就研究什么行静态列静态,先把基础的数据搬运节奏搞清楚。

先对齐一个常见误区:面试官问数据流,不是想听你把行静态、输出静态的定义背一遍,而是想看你有没有从带宽和片上存储的物理约束反推调度。我自己调过类似结构,最直观的体会是——利用率掉到60%,多半不是MAC阵列本身的问题,而是数据搬运节奏没对上。你可以从最基础的输出静态(OS)入手,别一上来就搞复杂的行静态列静态混合。OS流的本质是让每个输出像素的累加尽量在片上完成,这样输入特征图只需要过一遍行缓存。具体到RTL,Line Buffer要做成双缓冲:当前三行在算,下一行在预取,这样每个时钟周期都能稳定喂给MAC阵列新的输入数据,不会因为行缓存更新而空拍。权重这边,如果输出通道不多,直接全放寄存器堆,每个PE周期广播;如果输出通道多了,寄存器堆会爆炸,这时候才考虑把权重拆到BRAM,但要注意BRAM读端口数,一般一个BRAM一个周期只能读一个地址,多个PE要共享权重就得做多bank复制,这会额外吃LUT资源。等你把单层卷积调到90%以上,再去看池化层和卷积层之间的数据拼接——那才是真正掉利用率的地方,因为池化会改变数据有效周期,如果你还用原来的节奏灌数据,MAC阵列会有大量空转。个人建议,先拿一个3×3、输入输出各32通道的层练手,把OS流写顺了,再对比换成WS流看资源占用和时序变化。面试时如果能说出「我算过一笔账:一个MAC周期要喂两个操作数,Line Buffer读端口够不够、权重广播的扇出能不能时序收敛」这种话,基本就过关了。追问一句:你目前是卡在RTL实现阶段,还是已经调出波形但利用率上不去?这两者的排查方向差别挺大。

说个面试官大概率想听的判断维度:先看输入特征图尺寸和通道数的比例。大图小通道(比如第一层1024x1024x3),用输出静态,行缓存便宜,权重广播也简单;小图大通道(比如中间层32x32x256),用权重静态,让权重留在片上,输入行流式从DDR灌,否则行缓存做256通道会吃光BRAM。你那个60%利用率,大概率是层间切换时没做流水衔接——卷积算完到池化开始,中间隔了整幅图的写回和重读。解法是池化跟着卷积的最后一个有效输出走,别等整层结束。就这么简单,先按这个思路改,不行再聊DSP级联。

先泼一点冷水:你面试被问这个,对方大概率不是想听你把OS、WS的定义背出来,而是想看你在带宽和存储的物理约束下怎么做取舍。我自己调过类似结构,60%利用率最常见的原因不是数据流选错,而是层间衔接没有做流水。卷积算完,结果整幅写回DDR,池化再整幅读出来,这个来回搬运能把效率直接砍掉两成。最小可行路径是:先别动数据流,把池化拆成跟随卷积最后一个有效输出窗口走,也就是每个输出像素的3×3窗口算完,如果落在池化窗口的右下角,立刻做一次池化,不需要等整层结束。这一条改完,利用率一般能回到75%以上。再往下,才轮到选数据流。判断标准很简单:输入是大图小通道,比如第一层1024x1024x3,输出静态,行缓存三行x3通道,便宜;输入是小图大通道,比如32x32x256,权重静态,权重全留在片上,输入行流式灌,否则行缓存做256通道直接吃光BRAM。还有一个容易忽略的点:权重如果放BRAM,注意读端口数量,一个BRAM只有两个读口,输出通道并行度超过2就得拆多个BRAM或者用寄存器堆。面试时你能说出这个细节,比背概念有用得多。另外,DSP级联这事别忽略,两个DSP级联做对称卷积核能省一半乘法器,但要注意级联路径的时序余量,频率跑不高利用率再高也没用。你先按池化跟随卷积改一版,跑一下看数据,再回来讨论数据流切换的细节。你用的是哪家的器件?UltraScale和7系列的DSP结构不太一样,级联方式也有区别。

我想从RTL写法的角度多说一层,因为很多人嘴上说OS、WS,实际写代码时根本没把数据流体现在状态机里。以输出静态为例,它的本质是:把输出特征图的一个像素作为最内层循环,让这个像素的累加尽量在片上完成。具体到代码,你需要一个三重循环:输出行、输出列、输入通道累加。最内层每次做3×3乘加并累加到同一个寄存器,这个寄存器就是输出像素的累加器。但问题来了,输入数据怎么喂?如果你把输入特征图完整存进BRAM,然后按地址去读每个3×3窗口,那每次读三个数要花三个周期,MAC阵列就空转两个周期,利用率天然就到不了100%。所以行缓存不是可选项,是必须项——它让你每个周期都能从滑动窗口里读出新的输入像素。行缓存的实现也有讲究:一般来说,3×3卷积需要三行数据同时有效,每行缓存的深度等于特征图宽度。你写RTL时,常见做法是例化三个移位寄存器,每个宽度是输入通道数乘数据位宽,每来一个新像素,三行同时右移一位,这样每个时钟上升沿,三个行缓存的输出端正好是当前窗口的三行数据。但这里有个坑:移位寄存器在FPGA里是用LUT实现的,如果通道数多、数据位宽大,LUT消耗会非常夸张。比如32通道、16bit数据,一个行缓存就是512bit宽,三个就是1536个触发器加LUT,一层下来可能吃掉几千个LUT。所以实际工程里,行缓存往往改用BRAM实现,用读写地址来模拟滑动窗口,而不是用移位寄存器。BRAM实现行缓存的技巧是:每行一个BRAM,写地址是当前列号,读地址是当前列号加2(或者用三个不同偏移的读地址),这样每个周期能同时读出三行同一列的数据。但BRAM只有两个读口,三行就要三个BRAM,或者用伪双端口。等你把这些细节想清楚,再回头看数据流——你会发现OS流适合通道少、图大的层,因为行缓存宽度小;WS流适合通道多、图小的层,因为权重可以常驻寄存器堆,输入数据流式灌入不占存储。中间层怎么切换?我的做法是:在RTL里做一个配置寄存器,层与层之间用状态机切换数据流模式,切换时把行缓存和权重寄存器堆的内容全部清空,重新加载,这个切换周期是不可避免的开销,一般几十个周期,相比整层几百万个周期可以忽略。面试官如果追问到这一层,基本上你就可以从候选者里脱颖而出了。你目前是在写全连接的RTL还是已经跑通了单层卷积?如果单层还没调到90%,先别想多层流水的事。

我拿自己调过的一个真实案例说吧,可能比理论更有参考价值。当时做的是一个YOLO类检测网络的第一层,输入416x416x3,输出通道32,3×3卷积加ReLU加2×2池化。一开始我按教科书写的OS流,行缓存三行x3通道,权重全放寄存器,结果MAC利用率只有55%。排查了很久才发现,问题根本不在卷积本身,而在池化的时序安排上——我让卷积层算完一整幅特征图,写回BRAM,池化再从头读一遍。这一来一回,相当于每算一个输出像素,中间多了一次写和一次读的延迟,MAC阵列在等数据的时候全在空转。后来我把池化改成跟随式:卷积每算完一个3×3窗口,我就判断这个窗口是不是落在池化窗口的右下角,如果是,立刻把4个卷积结果做一次池化。这样池化几乎零成本地嵌在卷积的流水里,利用率直接跳到82%。再往后,我发现第一层其实更适合用WS流——因为输入通道只有3,权重只有3x3x3x32,总共不到1K个数,完全可以把权重全部锁在寄存器里,输入行流式从DDR灌,行缓存只做单通道的滑动窗口。这样一来,DDR带宽只花在读输入上,权重零搬运,利用率到了94%。所以我的建议是:你先别管什么静态不静态,先把你当前层的输入通道数、输出通道数、特征图尺寸三个数列出来,算一下权重总量和输入行缓存深度,哪个更小就固定哪个。这个判断做完,数据流自然就出来了。你现在的瓶颈是在第一层大图,还是在中间层大通道?这两个的解法完全不同。

面试官问这个,其实不是考你名词,是考你有没有把数据流当成调度问题来想。我建议你先别碰WS/OS的二分法,直接盯住一个指标:每个周期MAC阵列有没有两个有效操作数进来。60%利用率最常见的原因就是Line Buffer的读端口和权重广播节奏没对上。最小可行路径是:先做输出静态(OS),把输出像素的累加放最内层,输入特征图用双缓冲行缓存,当前三行在算、下一行在预取,保证每个周期喂一个新像素;权重如果输出通道少于64,直接寄存器堆广播,别用BRAM,因为BRAM单周期只能读一个地址,广播会卡住。这一套改完,单层3×3卷积到90%以上不难。池化层就拆成跟随卷积最后一个有效窗口走,别等整幅图写回再重读。等你把这条跑顺了,再去研究行静态列静态的混合,那才是给中间层大通道用的。你现在是卡在单层还是层间衔接?这个得先说清楚。

我换个角度说吧。你纠结数据流怎么选,但真正的瓶颈往往在DSP的级联方式上。FPGA里DSP48E1本身支持25×18的乘法,你如果单做9位乘9位,一个DSP只跑一个乘加,那利用率当然高不了——因为MAC阵列的利用率不光看数据流,还看每个DSP内部有没有做级联累加。常见做法是:把同一输出像素的3×3乘加结果,通过DSP的P级联端口串起来,这样9个乘法只需1个DSP的加法树,而不是每个乘法单独配一个累加器。这个改完,你会发现即使数据流没动,利用率也能提10到15个点。然后再回头看数据流:OS适合大图小通道,WS适合小图大通道,这条判断标准没问题,但有个坑——层间切换时权重重载会空转,一般要提前预取下一层权重到另一块BRAM,双缓冲权重。你如果面试被追问,把这个DSP级联和权重预取两点讲出来,比单纯背数据流定义强得多。顺便问一句,你现在用的器件是Ultrascale还是7系列?DSP的级联方式差别挺大的。

一条很短的补充:面试官问数据流,最想听的其实是「你有没有意识到MAC利用率本质是数据供给率」。你只要说清楚三件事就行——第一,每个周期需要两个新操作数,一个来自输入特征图,一个来自权重,你用什么结构保证这两个数同时到达;第二,你算过片上RAM的读端口数和MAC阵列规模的匹配关系,比如16个PE但BRAM只有单端口,那必然空转;第三,你打算用什么指标来实测利用率,是看DSP的valid信号占空比,还是看状态机里idle周期的比例。把这三句话讲明白,比背十个数据流名词都有用。
发表回答
登录后可在本页底部提交回答