4. 行固定数据流(Row Stationary):原理、动机与核心思想
行固定(Row Stationary,简称 RS)是 Eyeriss 架构中最核心的数据流策略。说实话,我第一次看到这个方案时,心里想的是:「这不就是把卷积拆成一行一行算吗?有什么特别的?」
后来真正动手做加速器设计,才发现这背后藏着不少门道。咱们今天就把 RS 数据流掰开揉碎了讲清楚。
4.1 为什么需要行固定?
先回顾一下卷积的本质。一个 3×3 的卷积核,要在输入特征图上滑动计算。传统做法是:
- 权重固定(Weight Stationary):把卷积核权重放在 PE 里不动,让输入数据流过来。好处是权重复用率高,但输入数据要反复搬运。
- 输出固定(Output Stationary):把部分和留在 PE 里累加,等所有计算完成再写回。好处是减少写回次数,但权重要频繁加载。
这两种方案都有个共同问题——数据搬运开销太大。我在做第一版加速器原型时,发现 DRAM 访问功耗占了总功耗的 60% 以上。你想想看,数据从 DRAM 搬到全局缓冲区,再搬到 PE 阵列,每一步都在烧能量。
行固定的动机很直接:能不能让数据在 PE 之间多复用几次,少去 DRAM 里取?
4.2 行固定的核心思想
说白了,RS 就是把卷积计算按「行」切分。每个 PE 负责计算输出特征图的一行(或几行),并且让输入数据和权重在 PE 阵列中按行流动。
我习惯用一个简单的例子来说明。假设输入特征图是 5×5,卷积核是 3×3,步长为 1:
输入特征图(5×5):
[ a11 a12 a13 a14 a15 ]
[ a21 a22 a23 a24 a25 ]
[ a31 a32 a33 a34 a35 ]
[ a41 a42 a43 a44 a45 ]
[ a51 a52 a53 a54 a55 ]
卷积核(3×3):
[ w11 w12 w13 ]
[ w21 w22 w23 ]
[ w31 w32 w33 ]
在 RS 数据流中,PE 阵列的分配方式是这样的:
- PE(0,0) 负责计算输出像素 (0,0),需要输入行 0~2 和卷积核行 0~2
- PE(0,1) 负责计算输出像素 (0,1),需要输入行 0~2 和卷积核行 0~2
- 以此类推,每个 PE 固定负责一行输出
关键来了:相邻 PE 之间共享输入数据。PE(0,0) 和 PE(0,1) 都需要输入行 0~2,只是列偏移不同。所以输入数据可以在 PE 之间「接力」传递,不用每次都从缓冲区重新读。
RS 的核心原则:每个 PE 固定处理输出特征图的一行,输入数据和权重在 PE 阵列中按行流水传递,最大化数据复用。
4.3 数据复用模式
RS 数据流实现了三种复用:
| 复用类型 | 复用对象 | 复用方式 | 效果 |
|---|---|---|---|
| 输入复用 | 输入特征图行 | 同一行数据在多个 PE 间传递 | 减少 DRAM 读取次数 |
| 权重复用 | 卷积核权重 | 同一卷积核在多个输出行间共享 | 减少权重加载开销 |
| 部分和复用 | 中间累加结果 | 同一输出行在 PE 内累加 | 减少写回次数 |
嗯,这里要注意:三种复用不是同时发生的,而是在不同维度上交错进行。我曾经在调试一个 8×8 PE 阵列时,发现输入复用率只有理论值的 60%,后来发现是数据传递的时序没对齐——相邻 PE 的启动时间差了 2 个周期,导致数据流断开了。
4.4 RS 的映射流程
把卷积映射到 RS 数据流,大致分三步:
- 行切分:把输出特征图按行分给不同的 PE 组。每组 PE 负责连续的多行输出。
- 列切分:在每组内,把每行输出按列分给不同的 PE。每个 PE 负责一个输出像素。
- 数据流水:输入数据从左侧流入 PE 阵列,权重从上方流入,部分和在 PE 内累加后向下传递。
我画了一张图来展示这个流程:
我的经验:在设计 RS 映射时,PE 阵列的行数最好等于卷积核的高度。比如 3×3 卷积就用 3 行 PE,这样输入数据可以完整复用。如果 PE 行数比卷积核高度多,反而会浪费——多出来的 PE 要等下一组数据,流水线会断。
4.5 RS 的优缺点
任何数据流都有 trade-off,RS 也不例外。
| 维度 | 优点 | 缺点 |
|---|---|---|
| 数据复用 | 输入、权重、部分和三种复用同时存在 | 复用模式复杂,调度逻辑开销大 |
| 存储需求 | PE 内只需缓存一行输入和一行权重 | 需要额外的行缓冲区来对齐数据 |
| 负载均衡 | 每行计算量均匀,PE 利用率高 | 边界行(第一行和最后一行)计算量少 |
| 扩展性 | 容易扩展到更大的 PE 阵列 | 行间依赖导致启动延迟 |
我记得有一次做 7×7 大卷积核的映射,RS 的启动延迟问题特别明显。因为每个 PE 要等上一行 PE 算完才能开始,整个阵列的启动时间占了总计算时间的 15%。后来我用了「流水线预填充」的技巧——让前几行 PE 提前加载数据,才把启动延迟压到 3% 以下。
4.6 什么时候该用 RS?
根据我的经验,RS 最适合以下场景:
- 卷积核尺寸较小(3×3 或 5×5):行数少,数据复用效率高
- 输入特征图较大:行内数据多,水平传递的收益明显
- 批量处理(batch)较大:多个 batch 可以流水执行,掩盖启动延迟
避坑提醒:如果卷积核尺寸很大(比如 11×11),RS 的行数会很多,导致 PE 阵列的垂直维度过长。这时候数据传递的延迟会累积,反而降低效率。我曾经在 11×11 卷积上硬套 RS,结果吞吐量还不如简单的输出固定方案。后来改用「分块行固定」——把大卷积核拆成多个 3×3 子块,才解决问题。
4.7 小结
行固定数据流的核心,就是让每个 PE 固定负责一行输出,然后让输入数据和权重在 PE 间流水传递。它不像权重固定那样死板,也不像输出固定那样被动——RS 是在「数据搬运」和「计算密度」之间找平衡。
你想想看,一个 3×3 卷积,如果用 RS 映射到 3×3 的 PE 阵列,输入数据只需要从 DRAM 读一次,然后在 PE 间水平传递 3 次,就能完成 9 个输出像素的计算。相比传统方案,DRAM 访问次数减少了 3 倍以上。这就是 RS 的价值所在。