存算一体架构下算子映射实战 - 第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模型来辅助决策。

知识体系总览

下面这张图,是我自己画的算子映射知识体系。你可以把它当作地图,随时回来看看。

算子映射知识体系 算子分类 卷积 数据复用率高 全连接 访存带宽瓶颈 池化 数据对齐问题 激活函数 非线性处理 核心挑战 数据复用:输入复用 / 权重复用 / 输出复用 计算并行度:数据供给 vs 计算能力 访存带宽:分块策略 / Roofline模型 映射策略 数据流优化 分块与调度 硬件感知映射 注:箭头表示知识关联,虚线表示映射关系 数据复用率 = 总计算量 / 数据加载量 复用率越高,能效越好

这张图把算子分类、核心挑战和映射策略串起来了。你每次做映射时,都可以回来看看——当前算子属于哪一类?主要挑战是什么?用什么策略应对?

我的习惯:拿到一个新网络,先画一张类似的图,把每层的算子和挑战标出来。这样整体方案就清晰了。你可以试试。

好了,算子映射基础就讲到这里。记住:卷积重复用,全连接重带宽,池化重对齐,激活函数重实现。把这四个记住了,映射就成功了一半。

想要更多资料,联系可可