2. 算子与计算模式:深度学习中的常见算子

好,咱们直接进入正题。这一章我打算聊聊深度学习里最常见的几个算子——Conv、GEMM、ReLU、Softmax。你可能会说,这些玩意儿谁不认识?但我要讲的不是它们怎么用,而是它们在存算一体架构下,到底是怎么“跑”起来的。

说白了,算子映射这件事,核心就两句话:算子的计算特征是什么?它的访存特征又是什么?搞明白这两点,你才能知道这个算子适不适合放到存算一体阵列上去做。

核心观点:算子的计算密度(Ops/Byte)决定了它是否适合存算一体。计算密度越高,越能发挥存算一体的优势。

2.1 卷积(Conv)—— 存算一体的“天选之子”

卷积,尤其是二维卷积,是CNN的绝对主力。我个人习惯把卷积拆成两个维度看:计算量和数据复用。

先看计算量。一个标准的3x3卷积,输入通道C_in,输出通道C_out,特征图大小HxW。它的计算量大约是:

MACs = C_out × C_in × 3 × 3 × H × W

这个数字通常很大。比如一个ResNet-50的中间层,一次卷积可能就要几亿次乘累加。嗯,这里要注意,计算量大不一定是坏事——对于存算一体来说,计算量大意味着你可以把权重固定在阵列里,反复使用。

再看数据复用。卷积有个特别好的性质:权重复用。同一个卷积核会在整个特征图上滑动,每个权重被反复读取。我在项目中遇到过一个问题:如果用传统架构做卷积,权重要从DRAM搬到SRAM,再搬到计算单元,功耗全花在搬数据上了。但存算一体呢?权重直接存在阵列里,数据一进来,计算就地完成。

我的经验:卷积的访存特征可以总结为“权重驻留、输入流式”。权重一旦加载到阵列,就别动了。输入数据按行流进来,输出结果按行流出去。这种模式,存算一体最擅长。

不过,卷积也有坑。比如padding和stride的处理。我曾经在映射一个空洞卷积时,发现数据流完全乱掉了——因为阵列的物理结构是规整的,而空洞卷积的输入采样是不连续的。解决办法?要么在数据加载时做重排,要么在阵列外围加一个地址映射逻辑。

2.2 矩阵乘法(GEMM)—— 存算一体的“基本功”

GEMM,说白了就是矩阵乘矩阵。全连接层、注意力机制里的QKV计算,本质上都是GEMM。

GEMM的计算特征很清晰:

C[M][N] = A[M][K] × B[K][N]

这里M、N、K通常都很大。比如一个BERT的中间层,M=512,K=768,N=768,一次GEMM就是512×768×768次乘累加。

GEMM的访存特征和卷积不太一样。卷积是权重复用,GEMM是输入和权重都复用。你想想看,矩阵A的一行要和矩阵B的所有列做内积,矩阵B的一列要和矩阵A的所有行做内积。这种“双复用”模式,对存算一体阵列的数据流设计提出了更高要求。

算子 计算密度(Ops/Byte) 访存模式 存算一体适配度
Conv (3x3) 高(~100+) 权重驻留,输入流式 ★★★★★
GEMM (大矩阵) 中高(~50-100) 双复用,需数据重排 ★★★★☆
ReLU 极低(~1) 逐元素,无复用 ★★☆☆☆
Softmax 低(~5-10) 行内规约,需全局通信 ★★★☆☆

我建议你在做GEMM映射时,重点关注数据分块。阵列大小是固定的,比如128×128。但你的矩阵可能是1024×1024。怎么分块?分块后怎么保证数据不冲突?这些都需要仔细设计。我曾经因为分块没做好,导致阵列利用率只有30%,后来改成“行主序+列主序”混合分块,利用率才提到85%以上。

2.3 ReLU —— 简单,但别小看它

ReLU,f(x)=max(0,x)。这玩意儿太简单了,简单到很多人觉得它不值得讨论。但我要说,在存算一体架构下,ReLU反而是一个“麻烦精”。

为什么?因为ReLU的计算密度太低了。它只需要一次比较操作,没有数据复用。你把它放到存算阵列里做,阵列的乘累加单元全在空转,利用率极低。

那怎么办?我的做法是:ReLU不做在阵列里,做在阵列外围的数字化处理单元上。数据从阵列出来后,经过ADC(模数转换),然后直接过一个比较器,输出0或原值。这样既不影响阵列的吞吐,又省了额外的访存。

注意:ReLU虽然简单,但它的数据依赖性可能会影响流水线设计。比如,ReLU的输出是下一层的输入,如果ReLU处理慢了,整个流水线就会卡住。我建议把ReLU和前一层的计算合并成一个“计算+激活”的宏操作,减少中间数据的搬移。

2.4 Softmax —— 最“反存算一体”的算子

Softmax,公式我就不写了,你肯定知道。它的问题是:需要全局规约。

什么意思?Softmax的计算分三步:求指数、求和、归一化。其中“求和”这一步,需要把一行里所有元素的值加起来。在传统架构里,这很简单——把数据读到寄存器,累加就行。但在存算一体架构里,数据是分布在阵列各个位置的,你要做全局求和,就得把数据全部读出来,送到一个累加器里。

这就尴尬了。存算一体的核心优势是“计算在存储内完成”,但Softmax迫使你把数据搬出来。说白了,Softmax的访存特征和存算一体是冲突的。

我遇到过最极端的情况:一个Transformer模型,Softmax占用了整个推理时间的40%。为什么?因为数据在阵列和外围处理单元之间来回搬了三次。后来怎么解决的?我用了近似Softmax——把指数运算换成查找表,把求和换成树形累加器,直接在阵列内部完成。虽然精度掉了0.1%,但速度提升了3倍。

避坑指南:如果你在做存算一体的Transformer加速,Softmax一定要单独优化。不要指望它能像Conv一样“天然适配”。我曾经因为没注意这一点,整个芯片流片回来后,Softmax成了性能瓶颈,改都没法改。

2.5 知识体系总览

说了这么多,我画了一张图,帮你把这一章的核心逻辑串起来。你看,算子的计算特征和访存特征,决定了它在存算一体架构下的映射策略。

算子映射核心逻辑 卷积 (Conv) 矩阵乘法 (GEMM) 激活函数 (ReLU) 归一化 (Softmax) 计算特征 高计算密度 权重复用 计算特征 中高计算密度 双复用模式 计算特征 极低计算密度 逐元素操作 计算特征 低计算密度 全局规约 访存特征 权重驻留 输入流式 访存特征 数据重排 分块加载 访存特征 无数据复用 可合并处理 访存特征 全局通信 数据搬移 阵列内直接映射 分块+重排映射 外围单元处理 近似+树形累加

这张图从左到右,展示了四种算子的完整映射链路。你看,Conv和GEMM是存算一体的“舒适区”,ReLU和Softmax则需要额外处理。搞明白这个逻辑,你就能针对不同算子,设计出最合适的映射方案。

好,这一章就到这里。记住一句话:算子的计算特征决定它能不能做,访存特征决定它怎么做。下一章,我会带你看看具体的映射流程和工具链,到时候咱们再细聊。

可可精品资料库