最近在做一个视频处理的AI加速项目,用Vitis HLS写的部分性能上不去,怀疑是数据交互方式有问题。我在C++侧用普通数组做中间缓存,但综合后BRAM占用爆炸,而且吞吐率比预期低很多。看了一些开源项目,发现大家好像更倾向用hls::stream来传数据。想问问2026年做FPGA AI加速的前辈们,实际工程中C++侧的数据结构到底怎么选?hls::stream和普通数组在资源占用和时序上差别有多大?另外,如果涉及多帧流水线,用hls::stream会不会有死锁风险?
2026年了,FPGA工程师做AI加速器,HLS和RTL混编时C++侧数组该用hls::stream还是普通数组?
6 个回答

先说你最可能踩的坑:普通数组在HLS里会被综合成BRAM或者寄存器堆,一旦索引不规律,综合器只能给你铺一堆BRAM,爆资源很正常。hls::stream本质是FIFO,数据流模式下一进一出,BRAM占用能小一个量级。我的建议是:视频处理这种行扫描、窗口滑动的算法,数据天然是流式的,直接用stream,别拿数组硬扛。多帧流水线确实要小心,stream深度一定要算准,比如一帧的行宽或者需要的行缓冲数,深度不够会反压,上游写不进去就死锁。你可以先用dataflow顶层架构把各模块串起来,模块内部再用stream,跑通了再回头优化单模块。你现在的吞吐率瓶颈是卡在数据搬运还是计算单元上?这个先分清,不然换数据结构也白搭。

这问题本质上是「空间换时间」和「时间换空间」的老话题,但2026年做AI加速器,你得把HLS当成一个「带时序约束的C编译器」来用,而不是当成C语言来写。普通数组在C++里是随机访问语义,HLS要把它映射到FPGA上,如果访问模式不固定,综合器只能生成多端口RAM或者复制多份BRAM来满足并行度,这就是你BRAM爆炸的根源。而hls::stream强制要求生产者和消费者按顺序交互,综合器能明确知道这是一个FIFO,于是可以做乒乓缓冲、双时钟域隔离,甚至自动做dataflow的流水线调度,资源利用率完全不同。你提的多帧流水线死锁问题,常见原因有三个:一是stream深度没设够,比如一帧数据是1920×1080,你写了个深度1024的stream,中间模块处理慢一点,上游写满就阻塞,整个链路卡死;二是dataflow模式下,某个模块的读条件不满足,比如if(empty)不写、if(full)不读,但你把这两个条件写反了,造成互相等待;三是用了非阻塞读hls::stream::read_nb但是没有处理返回false的情况,导致数据漏读,帧错位。实际工程里,我建议你分三步走:第一步先把顶层改成dataflow架构,每个模块内部暂时保留数组,先验证帧级流水能不能跑通;第二步把模块间交互改成stream,此时关注每个stream的深度,一般取「一行像素数」或者「一个tile的大小」,宁可偏大不要偏小,因为BRAM多占一点比死锁好查得多;第三步才是模块内部优化,把大数组拆成行缓冲,或者用hls::line_buffer库。另外提醒一句,Vitis HLS 2026版本对stream的默认深度是2,很多人忘了改,小数据量没事,视频帧一上来就卡。你现在的瓶颈如果确认是数据搬运,那stream基本是正解,但如果是计算单元吞吐不够,换stream也救不了。你方便说下目前是哪个模块吞吐率上不去吗?是卷积、池化还是后面的后处理?

说个我自己的经历吧。之前做过一个yolov5s的加速器,也是视频流输入,一开始图省事,中间层全用普通数组,结果BRAM用了百分之八十多,布局布线跑一天都过不了时序。后来痛定思痛,把层与层之间全改成hls::stream,BRAM直接降到百分之四十,时序也松快了。但代价是调试变难了,stream里看不到中间值,只能靠仿真波形去数FIFO的读写计数,定位bug比数组麻烦得多。所以我的建议是:如果你还在算法验证阶段,模块内部可以用数组,方便打印和检查;一旦进入性能优化阶段,模块之间的接口必须换成stream,这是硬性要求。多帧死锁还有一个坑,就是dataflow的调度是静态的,你如果用了条件分支,比如if(frame_id % 2 == 0)走A路,else走B路,两路处理时间不一样,stream的深度就得按最慢那路算,否则快的路会把慢的路的FIFO灌满。另外,2026年Vitis HLS其实有个不错的工具叫HLS Dataflow Viewer,可以看每个stream的占用率曲线,死锁前一般会有某个stream持续满的征兆,你下次跑不下去的时候打开看看。你现在的视频分辨率是多少?如果是1080p以上,建议stream深度至少设2048起步,别省这点BRAM。

其实你先别纠结选哪个,先看数据是不是真流式的。视频AI加速里行扫描、滑窗这种天生就是流,直接用stream,BRAM立马降下来。数组适合那种要反复跳着读的,比如注意力机制里的随机索引,那才用数组。你BRAM爆了八成是把流式数据硬塞数组里了。

说个实际点的判断方法吧。你先画数据流图,每个模块的输入输出是单生产者单消费者、顺序消费,那就该用stream;只要有随机访问、要回头读历史数据、或者多个消费者按不同顺序取数,stream就装不下,得退回数组或者自己用双口BRAM包一个带地址的FIFO。普通数组之所以爆BRAM,是因为HLS为了并行度会把数组拆成多块,你又没加array_partition指示,它默认给你复制好几份,每份都是完整大小,那当然爆炸。stream的好处是综合器明确知道是FIFO,可以做乒乓缓冲,两个bank轮流读写,不用等对方空出来,吞吐率自然上去。多帧死锁这事,我见过最典型的不是深度不够,而是dataflow里有个模块带条件返回,比如检测到坏帧就提前break,stream里残留半帧数据,下一帧来的时候读写指针错位,直接卡死。解决办法是每帧开始前把所有stream的clear()调一遍,或者干脆在顶层按帧粒度做同步,一帧一帧握手,别让dataflow跨帧乱调度。你先说下你的瓶颈是卡在行缓冲还是卡在跨模块传输吧,这两种改法完全不一样。

给你一个最省事的落地顺序吧。先把顶层接口全定义成hls::stream,模块内部你爱用数组用数组,反正内部数组综合完也就是BRAM,影响可控。跑通功能后,再用dataflow把各模块串起来,这时候注意stream深度别拍脑袋,按最慢模块处理一帧需要的缓冲量来算,比如你的模块一次要读三行做卷积,那stream深度至少得是3倍行宽,不然上游写满就反压,整个流水线停摆。等你dataflow跑通了,再去优化内部数组,这时候你才知道哪些数组能改成stream减少BRAM,哪些动不了。别一上来就全换stream,调试的时候中间值全在FIFO里,波形里数读写计数能数到怀疑人生。
发表回答
登录后可在本页底部提交回答