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 四者如何协同?一张图说清楚
下面这张图展示了四个策略的关系。循环分块决定数据怎么切,数据复用决定数据怎么存,流水线决定计算怎么排,并行度决定硬件怎么搭。它们互相影响,得一起调优。
嗯,这张图我画了好几次才满意。你看,箭头是双向的,说明它们互相制约。比如你并行度设高了,分块就得跟着调,不然Buffer放不下。
4.6 实战中的权衡
最后聊点实际的。我做过一个BERT训练加速器,当时面临一个选择:
- 方案A:大分块+高并行度,但数据复用率低
- 方案B:小分块+低并行度,但数据复用率高
理论上方案A峰值算力高,但实际跑起来方案B更快。为什么?因为方案A的DRAM带宽成了瓶颈,计算单元经常空等。方案B虽然算力低,但数据都在片上,利用率高。
结论:别盲目追求高并行度。先算清楚你的带宽够不够,再决定并行度。我现在的习惯是:先做Roofline分析,再定分块和复用策略,最后调流水线深度。
好了,数据流映射就聊到这儿。记住,这四个策略是组合拳,别单独用。下一章咱们聊硬件架构的具体实现,到时候你会看到这些策略是怎么落地到RTL里的。