存算一体架构下算子映射实战 - 第2节
算子映射基础:算子定义与分类
各位同学,今天我们来聊聊算子映射的基础。说实话,这个章节是整个课程的地基。你想想看,如果连算子都认不全,映射就无从谈起。
我个人习惯把算子分成四大类:卷积、全连接、池化、激活函数。这四类在存算一体架构下的行为差异很大,映射策略也完全不同。
卷积算子:存算一体的核心战场
卷积,说白了就是滑动窗口乘累加。我在项目中遇到过不少新手,一上来就问「卷积不就是矩阵乘法吗?」——嗯,还真不是。卷积有数据复用特性,这是存算一体架构最喜欢的地方。
卷积的核心参数:
- 输入通道(C_in)和输出通道(C_out)
- 卷积核尺寸(K×K),常见3×3、5×5
- 步长(stride)和填充(padding)
- 输入特征图尺寸(H×W)
举个例子,一个3×3卷积,输入是64×64×32,输出是64×64×64。你算算乘累加次数?
// 卷积计算量估算
// 输出特征图尺寸: 64×64
// 输出通道: 64
// 卷积核: 3×3
// 输入通道: 32
总乘累加次数 = 64 × 64 × 64 × 3 × 3 × 32 = 75,497,472 次
七千五百万次乘累加!这还只是一层。所以为什么存算一体对卷积友好?因为每次卷积核滑动,输入数据可以被多次复用。我做过一个项目,把卷积的输入特征图存在SRAM里,权重存在ReRAM crossbar上,数据复用率提升了4倍。
关键洞察:卷积的输入特征图复用率 = (K×K×C_out) / 1,权重复用率 = (H×W) / 1。说白了,特征图越大、卷积核越大,复用率越高。
全连接算子:带宽的噩梦
全连接,就是每个输入神经元和每个输出神经元都相连。我刚开始做存算一体时,觉得全连接很简单——不就是矩阵向量乘吗?后来发现,全连接是访存带宽的噩梦。
为什么?因为全连接没有数据复用!
- 输入向量:N个元素
- 权重矩阵:N×M个元素
- 输出向量:M个元素
每次计算,权重矩阵全部要读一遍。如果N=4096,M=4096,权重就是16M个参数。在存算一体架构里,这意味着一大笔访存开销。
避坑指南:我曾经在一个边缘AI芯片项目里,把全连接层直接映射到ReRAM阵列上。结果发现,权重更新频率太高,ReRAM的写寿命撑不住。后来我改用SRAM+数字计算单元来处理全连接,才解决了问题。
池化算子:简单但容易忽略
池化,最大池化、平均池化,说白了就是降采样。这个算子计算量不大,但数据流模式很特殊。
最大池化:取窗口内的最大值。平均池化:取窗口内的平均值。
池化的映射难点在哪?数据对齐。比如2×2池化,步长2,输出尺寸减半。在存算一体架构里,如果输入特征图是逐行存储的,池化窗口的边界处理就有点麻烦。
我记得有个项目,池化层的输入是32×32,池化窗口3×3,步长2。边界填充怎么处理?我建议直接用「忽略边界」策略,省掉填充逻辑,反正精度损失不大。
激活函数:非线性处理的挑战
激活函数,ReLU、Sigmoid、Tanh。在存算一体架构里,激活函数通常用数字电路实现,因为模拟域做非线性太困难了。
ReLU最简单:f(x) = max(0, x)。说白了就是判断正负,负数置零。
Sigmoid和Tanh就麻烦了。它们是指数函数,硬件实现成本高。我一般建议:能用ReLU就别用Sigmoid。如果非要用,查表法(LUT)是个折中方案。
// ReLU的硬件实现伪代码
if (input > 0) {
output = input;
} else {
output = 0;
}
// 就这么简单,一个比较器+一个多路选择器搞定
个人经验:我在一个语音唤醒芯片项目里,把激活函数全部换成ReLU和PReLU。精度只掉了0.3%,但硬件面积省了40%。你想想看,值不值?
算子映射的核心挑战
好,算子分类讲完了。接下来聊聊映射的核心挑战。说白了,就是三个问题:数据复用、计算并行度、访存带宽。
数据复用:存算一体的命脉
存算一体为什么能省功耗?因为数据不用来回搬。但前提是——数据要被复用。
数据复用分三种:
- 输入特征图复用:同一个输入数据被多个卷积核使用
- 权重复用:同一个权重被多个输入位置使用
- 输出特征图复用:同一个输出位置累加多个部分和
我做过一个实验:把卷积的输入特征图复用率从2倍提升到8倍,整体能效提升了3倍。所以,映射时优先考虑数据复用。
核心原则:能复用就别搬,能搬一次就别搬两次。这是存算一体架构的黄金法则。
计算并行度:别让计算单元闲着
计算并行度,说白了就是同时有多少个乘累加在干活。存算一体架构里,ReRAM crossbar天然支持并行计算——一个列线可以同时做多个乘累加。
但并行度不是越高越好。为什么?因为数据供给跟不上。你想想看,如果计算单元有1024个,但数据带宽只能喂饱256个,那剩下的768个就在空转。
我建议的做法:
- 先算数据带宽能支撑多少并行度
- 再根据算子特性调整并行度
- 最后做仿真验证
访存带宽:永远绕不开的瓶颈
访存带宽,这是所有架构师都头疼的问题。存算一体虽然减少了数据搬运,但并不是零搬运。
举个例子:一个卷积层,输入特征图256KB,权重512KB,输出特征图256KB。如果片上SRAM只有512KB,那输入和权重不能同时放进去。怎么办?分块(tiling)。
分块策略:
- 把输入特征图分成小块,比如16×16
- 每次只加载一个小块和对应的权重
- 计算完再加载下一个小块
我曾经在一个项目里,因为没做好分块,导致80%的时间都在等数据。后来重新设计了数据流,把分块大小从32×32改成16×16,带宽利用率从20%提升到75%。
注意:分块太小,控制开销大;分块太大,片上存不下。这个平衡点需要反复调。我一般用Roofline模型来辅助决策。
知识体系总览
下面这张图,是我自己画的算子映射知识体系。你可以把它当作地图,随时回来看看。
这张图把算子分类、核心挑战和映射策略串起来了。你每次做映射时,都可以回来看看——当前算子属于哪一类?主要挑战是什么?用什么策略应对?
我的习惯:拿到一个新网络,先画一张类似的图,把每层的算子和挑战标出来。这样整体方案就清晰了。你可以试试。
好了,算子映射基础就讲到这里。记住:卷积重复用,全连接重带宽,池化重对齐,激活函数重实现。把这四个记住了,映射就成功了一半。
想要更多资料,联系可可