4. 数据流映射策略:循环分块、数据复用、流水线与并行度分析

好,咱们进入正题。数据流映射,说白了就是怎么把计算任务“塞”进加速器里。你算法写得再漂亮,映射不好,性能直接腰斩。我这些年踩过的坑,十有八九都跟这个有关。

今天聊四个核心策略:循环分块(Tiling)数据复用(Data Reuse)流水线(Pipelining)并行度分析。它们不是孤立的,你得组合着用。

4.1 循环分块:把大问题切成小方块

为什么需要分块?因为片上存储(SRAM)太小了。你想想看,一个ResNet-50的权重就几十MB,而加速器的片上Buffer通常只有几MB。硬塞?塞不下的。

循环分块的核心思想:把大的循环维度切小,让数据能待在离计算单元近的地方

关键原则:分块大小要匹配硬件Buffer容量。分太大,数据溢出;分太小,控制开销爆炸。

举个例子,矩阵乘法 C[M][N] = A[M][K] * B[K][N]:

// 原始三重循环
for (int m = 0; m < M; m++)
  for (int n = 0; n < N; n++)
    for (int k = 0; k < K; k++)
      C[m][n] += A[m][k] * B[k][n];

// 分块后(Tm, Tn, Tk为分块大小)
for (int m = 0; m < M; m += Tm)
  for (int n = 0; n < N; n += Tn)
    for (int k = 0; k < K; k += Tk)
      for (int mm = m; mm < m+Tm; mm++)
        for (int nn = n; nn < n+Tn; nn++)
          for (int kk = k; kk < k+Tk; kk++)
            C[mm][nn] += A[mm][kk] * B[kk][nn];

你看,外层循环控制数据搬运,内层循环做计算。这样A和B的子块就能放进Buffer里反复用。

我的经验:分块大小别用死数字。我习惯先算一下:Buffer能放下 Tm*Tk + Tk*Tn + Tm*Tn 个元素吗?留点余量给中间结果。

4.2 数据复用:让数据多干几次活

数据复用,就是尽量减少从DRAM读数据的次数。DRAM访问一次耗能是SRAM的100倍以上,能不读就不读。

常见的复用模式有三种:

  • 输入复用(Input Stationary):把输入特征图固定在Buffer里,反复跟不同权重算。适合卷积层。
  • 权重复用(Weight Stationary):权重不动,输入流过来。适合全连接层。
  • 输出复用(Output Stationary):累加器不动,不断累加部分和。适合大矩阵乘法。

我做过一个项目,一开始用了输入复用,结果发现权重反复加载,功耗高得离谱。后来改成权重复用,功耗降了40%。选哪种复用方式,得看你的计算瓶颈在哪

避坑指南:我曾经在一个稀疏加速器上硬套输出复用,结果因为非零元素分布不均匀,计算单元利用率不到30%。后来改成动态调度,才把利用率拉到70%以上。

4.3 流水线:让每一级都别闲着

流水线,就是把计算过程拆成多个阶段,让它们同时干活。理想情况下,N级流水线能把吞吐提升N倍。

在训练加速器里,常见的流水线有:

  • 层间流水线:不同层在不同计算单元上并行。比如Conv1在PE Array上算,Pooling在专用单元上算。
  • 层内流水线:同一层的不同阶段并行。比如卷积的im2col、矩阵乘、偏置加、激活函数。
  • 微流水线:单个计算单元内部的寄存器级流水。这是RTL设计的事,但架构师得知道。

流水线最大的敌人是数据冒险。后一级等前一级的数据,流水线就停了。

解决思路:插入足够深的FIFO,或者做乱序调度。我个人偏好FIFO,简单可靠,乱序调度太费脑。

4.4 并行度分析:你到底能跑多快

并行度,就是一个时钟周期内能同时做多少次乘加运算。它直接决定了加速器的峰值算力。

分析并行度,我一般看三个维度:

维度 含义 典型值
空间并行 PE阵列的规模 16x16, 32x32
时间并行 流水线深度 3~8级
数据并行 同时处理的batch大小 1~64

但注意,理论峰值和实际吞吐是两码事。我见过一个设计,理论算力100 TOPS,实际跑起来只有30 TOPS。为什么?因为数据搬运跟不上。

所以并行度分析一定要结合Roofline模型。算力再高,带宽不够,也是白搭。

我的习惯:先画Roofline图,找到计算瓶颈还是带宽瓶颈。如果是带宽瓶颈,就别堆PE了,先优化数据复用和分块策略。

4.5 四者如何协同?一张图说清楚

下面这张图展示了四个策略的关系。循环分块决定数据怎么切,数据复用决定数据怎么存,流水线决定计算怎么排,并行度决定硬件怎么搭。它们互相影响,得一起调优。

数据流映射策略协同关系 循环分块 (Tiling) 决定数据如何切分 匹配硬件Buffer容量 影响数据搬运粒度 数据复用 (Data Reuse) 减少DRAM访问次数 输入/权重/输出复用 降低功耗的关键 流水线 (Pipelining) 层间/层内/微流水线 提升吞吐的关键 需解决数据冒险 并行度分析 空间/时间/数据并行 结合Roofline模型 理论峰值≠实际吞吐 分块大小 决定复用粒度 复用模式 影响流水线设计 流水线深度 决定时间并行 并行度 约束分块策略 四者协同调优,才能达到最佳性能

嗯,这张图我画了好几次才满意。你看,箭头是双向的,说明它们互相制约。比如你并行度设高了,分块就得跟着调,不然Buffer放不下。

4.6 实战中的权衡

最后聊点实际的。我做过一个BERT训练加速器,当时面临一个选择:

  • 方案A:大分块+高并行度,但数据复用率低
  • 方案B:小分块+低并行度,但数据复用率高

理论上方案A峰值算力高,但实际跑起来方案B更快。为什么?因为方案A的DRAM带宽成了瓶颈,计算单元经常空等。方案B虽然算力低,但数据都在片上,利用率高。

结论:别盲目追求高并行度。先算清楚你的带宽够不够,再决定并行度。我现在的习惯是:先做Roofline分析,再定分块和复用策略,最后调流水线深度。

好了,数据流映射就聊到这儿。记住,这四个策略是组合拳,别单独用。下一章咱们聊硬件架构的具体实现,到时候你会看到这些策略是怎么落地到RTL里的。


专注资料整理