数据流与复用策略:四种方案的对比与选择
大家好,我是做深度学习加速器出身的老工程师。今天咱们聊聊数据流和复用策略。
说实话,这可能是整个卷积加速器设计里最核心的决策之一。你想想看,同样的卷积计算,数据流选对了,功耗能差好几倍,面积也能省不少。我在项目里见过太多人一上来就写代码,结果后面发现带宽不够、缓存爆炸,回头改数据流,那叫一个痛苦。
为什么数据流这么重要?
卷积的本质是什么?说白了就是一堆乘加操作。但问题在于,这些操作涉及的数据量太大了——输入特征图、权重、输出特征图,三者加起来动不动就是几MB甚至几十MB。
而我们的片上缓存(SRAM)通常只有几百KB。这就意味着,数据必须在DRAM和SRAM之间来回搬运。搬运一次,功耗就上去一次。我有个项目,最初没注意数据流,结果DRAM访问功耗占了总功耗的70%以上,后来换了数据流,直接降到30%。
所以,数据流策略的核心目标就一个:最大化数据复用,最小化DRAM访问。
四种经典数据流策略
咱们一个一个来看。我按自己的理解,把它们分成四类:
| 策略名称 | 核心思想 | 谁被驻留在片上 | 典型场景 |
|---|---|---|---|
| 输入驻留 | 输入特征图不动,权重流式加载 | 输入特征图 | 输入通道数少、输出通道数多 |
| 权重驻留 | 权重不动,输入特征图流式加载 | 权重 | 权重固定、批量推理 |
| 输出驻留 | 部分和不动,输入和权重流式加载 | 部分和(输出) | 输出通道数少、输入通道数多 |
| 无复用 | 所有数据都从DRAM加载 | 无 | 仅用于理论对比 |
输入驻留(Input Stationary)
这个策略,我习惯叫它「把输入焊死在片上」。什么意思呢?就是输入特征图加载到SRAM后,尽量不动它,让所有需要用到这个输入图的权重轮流过来计算。
举个例子:假设输入特征图是 14×14×32,权重是 3×3×32×64。输入驻留的做法是:先把 14×14×32 的输入图全部放到片上SRAM,然后每次从DRAM加载一组 3×3×32 的权重,计算完一组输出通道,再加载下一组。
优点:输入特征图的复用率极高。每个输入像素会被所有输出通道的权重用到,复用次数等于输出通道数。
缺点:片上SRAM需要能放下整个输入特征图。如果输入图很大(比如 224×224×3),SRAM可能放不下。
我踩过的坑:曾经有个项目,输入图是 56×56×64,我天真地以为SRAM能放下。结果发现还要留空间给权重和部分和,最后SRAM爆了。后来我学乖了,先算一下:输入图大小 = 56×56×64×2字节 ≈ 400KB,加上其他开销,至少需要512KB SRAM。如果片上只有256KB,那就得考虑分块(tiling)了。
权重驻留(Weight Stationary)
这个策略正好反过来。权重是固定的,输入特征图流式加载。你想想看,在推理场景下,权重是训练好的,不会变。所以把权重放在片上,让输入图流过来,是不是很合理?
我在做边缘端加速器时特别喜欢用这个策略。因为边缘端的模型通常不大,权重可以全部放在片上。比如一个 3×3×32×64 的权重,大小才 3×3×32×64×2 ≈ 36KB,完全放得下。
优点:权重复用率极高。每个权重会被所有输入位置用到,复用次数等于输出特征图的像素数。
缺点:如果模型很大(比如ResNet-50,权重几十MB),权重驻留就不现实了,必须分批次加载。
我的建议:如果你的模型是固定的,而且片上SRAM能放下所有权重,那就用权重驻留。我做过一个项目,用权重驻留后,DRAM访问量减少了80%,功耗直接降了一半。
输出驻留(Output Stationary)
这个策略稍微绕一点。它的核心是:把部分和(partial sum)留在片上,不断累加,直到得到最终的输出值。
为什么需要这个?因为卷积计算是逐通道累加的。比如输入有32个通道,每个通道算完一部分,得到一个部分和,最后32个部分和累加在一起才是最终输出。
输出驻留的做法是:在片上开辟一块空间放部分和,然后每次加载一个输入像素和对应的权重,计算后累加到部分和上。等所有通道都算完了,部分和就变成了最终输出,写回DRAM。
优点:部分和的复用率极高。每个部分和会被累加多次,次数等于输入通道数。
缺点:片上需要同时存放多个输出通道的部分和。如果输出通道数很多,SRAM压力也大。
注意:输出驻留有个隐藏问题——部分和的精度。如果累加次数太多(比如几百次),浮点数的精度损失会累积。我遇到过一个问题,累加1000多次后,误差从1e-6涨到了1e-3,虽然对最终结果影响不大,但心里总是不舒服。后来我改用定点数,配合适当的缩放,才解决了这个问题。
无复用策略(No Reuse)
这个策略,说白了就是「暴力计算」。每次计算需要的所有数据,都从DRAM加载。算完一次,数据就丢掉,下次再加载。
你可能会问:谁会这么干?嗯,理论上没人会这么干。但我在做性能建模时,经常用无复用策略作为baseline。它代表的是最差情况——DRAM带宽完全成为瓶颈。
举个例子:一个 3×3 卷积,每次计算需要 9 个输入像素和 9 个权重。如果无复用,每算一次就要从DRAM读 18 个数。而有复用的话,很多数据可以重复使用,DRAM访问量能减少 10 倍以上。
结论:无复用策略只适合做理论对比,实际设计中千万别用。
如何选择?一张图看懂
下面这张图是我自己画的,总结了四种策略的适用场景。你可以把它当作决策树来用。
实际项目中的选择经验
好了,理论讲完了,说说我在实际项目中怎么选的。
场景一:边缘端推理芯片
模型小(MobileNet、ShuffleNet),SRAM通常有256KB-512KB。我一般用权重驻留。因为权重可以全部放片上,输入图流式加载,DRAM带宽压力小。功耗也低。
场景二:云端训练芯片
模型大(ResNet-152、ViT),SRAM虽然大(几MB),但权重和输入图都放不下。这时候我会混合使用:对于浅层(输入图大、通道少),用输入驻留;对于深层(输入图小、通道多),用输出驻留。
场景三:视频处理芯片
输入是视频流,连续帧之间有很多重复数据。我试过用输出驻留,配合帧间复用,效果不错。但要注意,视频处理对延迟要求高,输出驻留的累加过程可能会引入额外延迟。
核心总结:没有完美的数据流策略。选择的关键在于——你的瓶颈是什么?是DRAM带宽?是SRAM大小?还是计算单元的利用率?想清楚这个,策略自然就出来了。
避坑指南
最后,分享几个我踩过的坑:
- 我曾经以为数据流选一次就够了。结果发现,同一个网络的不同层,最优数据流完全不同。后来我做了个自动选择器,每层单独选策略,性能提升了30%。
- 我曾经忽略过数据搬移的功耗。只关注了计算单元的利用率,结果DRAM访问功耗爆表。记住:数据搬移的功耗远大于计算功耗。
- 我曾经在输出驻留时忘记考虑部分和的位宽。累加次数多了,位宽不够,结果溢出。后来我提前算好最大累加次数,给部分和多留了4位。
嗯,今天就聊到这里。数据流这东西,纸上谈兵容易,真正做起来坑不少。希望我的经验能帮你少走弯路。
可可精品资料库