数据流与架构映射:Row-Stationary 数据流深度解析

大家好,我是你们的老朋友。今天我们来啃一块硬骨头——Eyeriss 架构里的 Row-Stationary(RS)数据流。说实话,我第一次看时也被绕得有点晕,但后来在项目里亲手调过几版数据流之后,才真正体会到 RS 设计的精妙之处。

咱们先搞清楚一个核心问题:为什么数据流这么重要?

你想想看,在卷积神经网络加速器里,计算本身并不复杂——无非就是乘加运算。真正的瓶颈在哪?是数据搬运。数据从 DRAM 搬到全局缓存,再搬到 PE 阵列,每一级搬运都伴随着巨大的能耗和延迟。我见过不少团队,算法模型跑得飞快,一上硬件就卡在带宽上,说白了就是数据流没选对。

传统的数据流方案有三种:Input-Stationary、Output-Stationary、Weight-Stationary。它们各有侧重,但都只针对某一种数据做最大化复用。而 RS 的思路不一样——它试图在输入、输出、权重三者之间找到一个平衡点。

三种传统数据流的优缺点

我们先快速过一遍传统方案,这样你才能理解 RS 到底好在哪。

数据流类型 核心思想 优点 缺点
Input-Stationary 输入特征图固定在 PE 内,权重和部分和在 PE 间流动 输入复用率高,适合输入通道数多的层 权重频繁加载,能耗高;输出部分和累加逻辑复杂
Output-Stationary 输出部分和固定在 PE 内,输入和权重在 PE 间流动 输出累加简单,适合输出通道数多的层 输入和权重都需要频繁广播,带宽压力大
Weight-Stationary 权重固定在 PE 内,输入和部分和在 PE 间流动 权重复用率高,适合权重大的层 输入数据广播开销大,部分和传递路径长

关键洞察: 没有一种数据流能通吃所有网络层。实际项目中,卷积层、全连接层、深度可分离层对数据复用的需求完全不同。我曾经在一个项目里硬套 Weight-Stationary 去跑 MobileNet,结果性能惨不忍睹——因为深度可分离卷积的权重本来就少,复用优势根本发挥不出来。

Row-Stationary 的核心思想

RS 的出发点很简单:把卷积计算拆成行级别的粒度。

具体来说,每个 PE 负责计算输出特征图中一行的一部分。这一行对应的输入行和权重行都被映射到同一个 PE 上,数据在 PE 内部完成复用,不需要频繁跨 PE 搬运。

我习惯用一句话概括 RS:让数据在 PE 内部多待一会儿,少在外面跑。

为什么是行?不是列,也不是单个像素?

嗯,这里有个权衡。如果粒度太细(比如像素级),控制逻辑会变得极其复杂,而且数据复用机会少。如果粒度太粗(比如整个特征图),PE 内部存储又不够。行粒度刚好——既能利用卷积核在行方向上的滑动复用,又不会让 PE 的本地存储爆炸。

个人经验: 我在做 7x7 大卷积核映射时,发现 RS 的优势特别明显。因为大卷积核的行内复用次数多,数据在 PE 内部可以反复使用,外部带宽压力骤降。但如果是 1x1 卷积,RS 的优势就不那么突出了——这时候 Output-Stationary 反而更直接。

RS 的数据复用机制

RS 实现了三种数据复用:

  1. 输入行复用: 同一个输入行会被多个卷积核窗口共享。比如卷积核大小为 3,步长为 1,那么一行输入会被三个不同的输出行用到。
  2. 权重行复用: 同一个权重行在滑动窗口内被反复使用,直到覆盖完一整行输入。
  3. 部分和复用: 部分和在 PE 内部累加,不需要频繁写回全局缓存。

这三种复用叠加起来,效果非常可观。我给你们算一笔账:

假设输入特征图尺寸为 14x14,卷积核 3x3,输出通道 64。如果用 Input-Stationary,输入数据只需要加载一次,但权重需要加载 64 次(每个输出通道一次)。如果用 RS,每个 PE 负责一行输出,权重和输入在行内充分复用,整体数据搬运量能减少 40%-60%。

避坑指南: 我曾经在映射 ResNet-50 时,想当然地把所有层都用同一种 RS 配置。结果发现某些层的 PE 利用率只有 60%。后来才意识到,不同层的输入输出尺寸差异很大,需要动态调整行映射的宽度。记住:RS 不是银弹,它需要配合层感知的调度策略。

RS 与传统方案的对比

为了让你更直观地理解差异,我画了一张对比图:

四种数据流对比:数据搬运路径 Input-Stationary 输入固定,权重广播 权重搬运次数 = 输出通道数 Output-Stationary 输出固定,输入广播 输入搬运次数 = 输入通道数 Weight-Stationary 权重固定,输入广播 输入搬运次数 = 输出特征图尺寸 Row-Stationary 行内复用,三者平衡 搬运次数减少 40%-60% RS 的核心优势:通过行级粒度,让输入、权重、部分和三种数据都在 PE 内部完成最大程度的复用 减少了跨 PE 和跨层的数据搬运,从而降低能耗和带宽压力 数据复用维度对比 复用类型 Input-Sta Output-Sta Weight-Sta Row-Sta 输入复用 高 低 低 高 权重复用 低 低 高 高 部分和复用 中 高 中 高 整体能效 中 中 中 高

RS 在 Eyeriss 中的具体实现

Eyeriss 的 PE 阵列是 14x12 的二维网格。每个 PE 内部有一个小的寄存器文件,用来存储当前行计算所需的输入和权重。

映射过程大致是这样的:

  1. 把输出特征图按行切分,每行分配给一个 PE 行(或者一组 PE)。
  2. 每个 PE 加载对应的输入行和权重行到本地寄存器。
  3. PE 在行内滑动计算,产生部分和,累加到本地。
  4. 当一行计算完成后,部分和传递到下一个 PE 或者写回全局缓存。

这里有个细节:Eyeriss 的 PE 之间是有数据通路的,部分和可以在 PE 间直接传递,不需要每次都经过全局缓存。这个设计大大减少了 DRAM 访问次数。

性能数据: 根据 Eyeriss 的数据,相比传统的 Output-Stationary 方案,RS 在 AlexNet 上能效提升约 1.4x-2.5x,在 VGG-16 上提升约 1.3x-2.0x。具体提升幅度取决于网络层的尺寸和卷积核大小。

什么时候该用 RS?

我个人的经验是:

  • 大卷积核(5x5、7x7): RS 优势明显,行内复用次数多。
  • 中等尺寸特征图(14x14、28x28): RS 表现稳定,PE 利用率高。
  • 小特征图(7x7 以下): RS 的优势减弱,可以考虑 Output-Stationary。
  • 1x1 卷积: 行内没有滑动复用,RS 退化为类似 Weight-Stationary 的行为,不如直接用 Output-Stationary。

实用建议: 如果你在设计自己的加速器,不要只盯着一种数据流。我建议做一个数据流调度器,根据每层的特征动态选择最优方案。Eyeriss 的 RS 已经做得很好,但如果你能结合层感知的调度,性能还能再提升 10%-20%。