第三章:数据流分析——DDG构建、读写依赖与数据复用

各位同学,今天我们来聊聊数据流分析。说实话,这章内容是我个人觉得整个算子融合策略里最「烧脑」但也最「值钱」的部分。为什么?因为你能不能把融合做好,很大程度上取决于你对数据流的理解有多深。

我刚开始做存算一体芯片时,踩过一个坑:明明两个算子可以融合,结果因为没分析清楚数据依赖,导致读写冲突,性能反而下降了30%。嗯,从那以后,我养成了一个习惯——先画数据依赖图,再谈融合。

3.1 数据依赖图(DDG)的构建

数据依赖图,说白了就是一张「谁等谁」的关系图。每个节点代表一个操作(比如矩阵乘、激活函数),每条边代表数据流动的方向。

构建DDG时,我个人习惯分三步走:

  1. 识别所有操作节点:把计算图中的每个算子都列出来
  2. 标记数据流向:每个算子的输入来自哪里,输出去往哪里
  3. 建立依赖边:如果算子B的输入依赖算子A的输出,就画一条从A到B的边

举个例子,一个简单的卷积+ReLU+池化流水线:

// 伪代码表示DDG构建过程
Node: Conv2D (input: feature_map, weight)
Node: ReLU (input: Conv2D_output)
Node: MaxPool (input: ReLU_output)

Edges:
  Conv2D_output → ReLU_input
  ReLU_output → MaxPool_input

你想想看,这个图一画出来,融合机会就一目了然了。Conv2D和ReLU之间没有数据重排,完全可以合并成一个算子。

核心要点:DDG的粒度决定了你能发现多少融合机会。我建议把粒度细化到「单个数据元素」级别,而不是「整个张量」级别。虽然构建起来麻烦点,但后续优化空间大得多。

3.2 读写依赖分析

读写依赖分析,是DDG的进阶版。它要回答三个问题:

  • 读后写(RAW):后面的操作要读前面的结果,前面的结果还没写完怎么办?
  • 写后读(WAR):后面的操作要写数据,但前面的操作还在读旧数据?
  • 写后写(WAW):两个操作都要写同一个位置,谁覆盖谁?

我在项目中遇到过最典型的RAW问题:两个卷积层共享同一个输入特征图,第一个卷积还没算完,第二个卷积就开始读数据了。在传统架构里这问题不大,但在存算一体里,存储器和计算单元是紧耦合的,读写冲突会直接导致计算停顿。

避坑指南:我曾经因为没处理好WAR依赖,导致一个3层融合的网络在芯片上跑出了错误结果。排查了整整两天才发现,是后面的算子提前把中间结果覆盖了。所以,我建议你在做融合前,先做一个完整的依赖分析表。

下面是一个简单的依赖分析表示例:

操作对 依赖类型 是否可融合 备注
Conv2D → ReLU RAW 是 ReLU只读不写,无冲突
Conv2D → Conv2D RAW 有条件 需检查kernel大小和stride
ReLU → MaxPool RAW 是 流水线式处理
Conv2D → Conv2D (共享权重) WAW 否 权重更新冲突

3.3 数据复用机会识别

数据复用,是存算一体芯片性能提升的关键。为什么?因为存算一体的核心优势就是减少数据搬运。如果你能识别出哪些数据可以被多次使用,就能大幅降低访存开销。

我个人总结了三类数据复用机会:

  • 时间复用:同一个数据在不同时间步被多次使用。比如卷积核的权重,在滑动窗口过程中被反复读取。
  • 空间复用:同一个数据被多个计算单元同时使用。比如广播操作中的标量。
  • 流水线复用:前一个算子的输出直接作为后一个算子的输入,中间不需要写回存储器。

你想想看,如果能同时利用这三种复用,那数据搬运量能减少一个数量级。我记得有一次优化一个ResNet-50的推理过程,通过识别流水线复用,把中间结果的写回次数从12次降到了3次,性能提升了将近40%。

实用技巧:在构建DDG时,可以给每个数据流打一个「复用标签」。比如:

  • 标签1:只使用一次,建议直接丢弃
  • 标签2:使用2-4次,建议留在本地缓存
  • 标签3:使用5次以上,建议固化到存算阵列中

这样在后续做融合决策时,就能快速判断哪些数据值得「留下来」。

3.4 数据局部性优化

数据局部性,说白了就是「让数据离计算越近越好」。在存算一体架构里,这个原则尤其重要——因为计算单元和存储单元是物理上绑定的。

我常用的优化策略有四个:

  1. 空间局部性:把相邻的数据放在相邻的存储单元里。比如卷积操作中,把同一个通道的特征图连续存放。
  2. 时间局部性:把短时间内会被多次访问的数据放在高速缓存里。比如循环中的常量。
  3. 计算局部性:把有依赖关系的算子安排在物理上靠近的位置。比如Conv2D和ReLU放在同一个存算块里。
  4. 数据重排:改变数据的存储顺序,使其更符合计算模式。比如把NHWC格式转成NCHW格式。

嗯,这里要注意一点:数据重排虽然能提升局部性,但重排本身也有开销。我曾经在一个项目中,为了追求极致的局部性,做了大量的数据重排,结果重排的时间比计算时间还长。所以,一定要权衡利弊。

下面这张图展示了数据局部性优化的核心逻辑:

数据局部性优化流程 原始数据 数据重排 NHWC → NCHW 存算阵列 计算+存储 局部性分析 空间局部性 | 时间局部性 计算局部性 | 数据复用 优化后的数据流 迭代优化

从这张图可以看出,数据局部性优化是一个迭代过程。先做数据重排,然后分析局部性,最后根据分析结果调整存储布局。如果效果不理想,就再重排一次。

总结一下:数据流分析是算子融合的基础。DDG帮你看到全局依赖关系,读写依赖分析帮你避免冲突,数据复用识别帮你找到优化机会,数据局部性优化帮你把理论变成实践。这四个步骤环环相扣,缺一不可。

好了,这一章的内容就到这里。记住我的一句话:数据流分析做得越细,融合策略就越稳。下一章我们会讲具体的融合算法,到时候你会发现,今天学的这些分析技巧全都能用上。

课后练习:找一个简单的卷积神经网络(比如LeNet-5),手动构建它的DDG,并标出所有RAW、WAR、WAW依赖。然后看看哪些算子可以融合,哪些不能。做完这个练习,你对数据流分析的理解会更深一层。

可可精品资料库