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则需要额外处理。搞明白这个逻辑,你就能针对不同算子,设计出最合适的映射方案。
好,这一章就到这里。记住一句话:算子的计算特征决定它能不能做,访存特征决定它怎么做。下一章,我会带你看看具体的映射流程和工具链,到时候咱们再细聊。