脉动阵列(1):基础知识

脉动阵列(Systolic Array)

1. 介绍

本目录提供一个 二维脉动阵列 的 Python 仿真,用于计算矩阵乘法。

  • 输入矩阵:A 的形状是 (m, k),B 的形状是 (k, n)
  • 输出矩阵:C = A @ B,形状是 (m, n)

阵列由 m × n 个处理单元(PE)组成。每个 PE 在每个周期执行一次乘加(MAC):

  • acc[i, j] = acc[i, j] + in_a * in_b
  • in_a 向右传递
  • in_b 向下传递

systolic_array

注意事项

参考代码中和文档的叙述中,都是以不先存储矩阵(非权重驻留)的方法展示。

假如绿色部分是矩阵 L,蓝色部分是矩阵 T,输出矩阵为 C。由上图你可以看出,要生成 C 的某一行,需要知道整个矩阵 T 的内容,所以在看到完整的 T 之前,你实际上无法生成 C 的任何一行。

同理,要生成 C 的某一列,需要知道整个矩阵 L 的内容,所以在看到完整的 L 之前,你实际上无法生成 C 的任何一列。

正因如此,在真实系统中,通常是先把整个矩阵 L 输入到脉动阵列中并存储起来(权重驻留),然后才开始处理矩阵 T 的各行;或者把整个矩阵 T 输入到脉动阵列中并存储起来,然后才开始处理矩阵 L 的各列。

下图展示了把整个矩阵 L 输入到脉动阵列中并存储起来,然后才开始处理矩阵 T 的各行的情况。

注意: 上面这副图的蓝色矩阵的 Spatial Skew 是搞错了的。


2. 关键规律(与代码一致)

对于 A(m×k) @ B(k×n):

  • 输入倾斜(skew)规则

    • A[i, p] 在周期 t = i + p 注入第 i 行左侧
    • B[p, j] 在周期 t = j + p 注入第 j 列上侧
  • 脉动阵列计算矩阵总耗费周期数

    • total_cycles = m + n + k - 2

3. 代码说明(systolic_array.py)

  • PE (脉动阵列 cell)

    • compute(in_a, in_b):执行乘加,保存下一拍输出
    • clock_tick():更新 out_a 和 out_b
  • generate_skewed_inputs(A, B)

    • 根据 A、B 生成倾斜输入序列 A_inputs 与 B_inputs
    • 返回 A_inputs、B_inputs、total_cycles
  • simulate_systolic_array(A, B)

    • 构建 m × n 的 PE 网格
    • 每个周期执行“先 compute、后 clock_tick”
    • 返回 C、history、A_inputs、B_inputs
  • print_cycle_history(history)

    • 打印每个周期的 PE 累加值
  • plot_systolic_history(history, m, n)

    • 可视化每个周期的数据输入、PE 状态和数据流向

4. 使用方式

python .\systolic_array.py

运行后会:

  1. 生成并打印 A_inputs / B_inputs
  2. 打印每个周期的累加状态
  3. 输出 C 与 A @ B 对比结果
  4. 绘制每个周期的数据流动图

5. 补充

假设:

  • 绿色部分对应矩阵 LL
  • 蓝色部分对应矩阵 TT
  • 输出矩阵为 CC

并且有:

C=L×TC = L \times T

5.1 矩阵定义

矩阵 LL 是一个 2×22 \times 2 矩阵:

L=[l00l01l10l11]L = \begin{bmatrix} l_{00} & l_{01} \\ l_{10} & l_{11} \end{bmatrix}

矩阵 TT 是一个 2×22 \times 2 矩阵:

T=[t00t01t10t11]T = \begin{bmatrix} t_{00} & t_{01} \\ t_{10} & t_{11} \end{bmatrix}

输出矩阵 CC 也是一个 2×22 \times 2 矩阵:

C=[c00c01c10c11]C = \begin{bmatrix} c_{00} & c_{01} \\ c_{10} & c_{11} \end{bmatrix}


5.2 矩阵乘法展开

由

C=L×TC = L \times T

可得:

c00=l00t00+l01t10c01=l00t01+l01t11c10=l10t00+l11t10c11=l10t01+l11t11\begin{aligned} c_{00} &= l_{00} t_{00} + l_{01} t_{10} \\ c_{01} &= l_{00} t_{01} + l_{01} t_{11} \\ c_{10} &= l_{10} t_{00} + l_{11} t_{10} \\ c_{11} &= l_{10} t_{01} + l_{11} t_{11} \end{aligned}


5.3 为什么生成 CC 的某一行需要知道整个矩阵 TT

例如,C 的第 0 行为:

[c00c01]\begin{bmatrix} c_{00} & c_{01} \end{bmatrix}

其中:

c00=l00t00+l01t10c_{00} = l_{00} t_{00} + l_{01} t_{10}

c01=l00t01+l01t11c_{01} = l_{00} t_{01} + l_{01} t_{11}

可以看到,要计算 c00c_{00} 和 c01c_{01},不仅需要矩阵 LL 的第 0 行:

[l00l01]\begin{bmatrix} l_{00} & l_{01} \end{bmatrix}

还需要矩阵 TT 的第 0 列和第 1 列:

[t00t10],[t01t11]\begin{bmatrix} t_{00} \\ t_{10} \end{bmatrix}, \quad \begin{bmatrix} t_{01} \\ t_{11} \end{bmatrix}

也就是说,要生成 CC 的某一行,实际上需要知道矩阵 TT 的完整内容。


5.4 为什么生成 CC 的某一列需要知道整个矩阵 LL

例如,C 的第 0 列为:

[c00c10]\begin{bmatrix} c_{00} \\ c_{10} \end{bmatrix}

其中:

c00=l00t00+l01t10c_{00} = l_{00} t_{00} + l_{01} t_{10}

c10=l10t00+l11t10c_{10} = l_{10} t_{00} + l_{11} t_{10}

可以看到,要计算 c00c_{00} 和 c10c_{10},不仅需要矩阵 TT 的第 0 列:

[t00t10]\begin{bmatrix} t_{00} \\ t_{10} \end{bmatrix}

还需要矩阵 LL 的第 0 行和第 1 行:

[l00l01],[l10l11]\begin{bmatrix} l_{00} & l_{01} \end{bmatrix}, \quad \begin{bmatrix} l_{10} & l_{11} \end{bmatrix}

也就是说,要生成 CC 的某一列,实际上需要知道矩阵 LL 的完整内容。


5.5 结合脉动阵列的说明

正因如此,在真实系统中,通常会采用以下两种方式之一:

方式 1:先存储整个矩阵 LL

先将整个矩阵 LL 输入到脉动阵列中并存储起来:

L=[l00l01l10l11]L = \begin{bmatrix} l_{00} & l_{01} \\ l_{10} & l_{11} \end{bmatrix}

然后再开始处理矩阵 TT 的各行。

方式 2:先存储整个矩阵 TT

先将整个矩阵 TT 输入到脉动阵列中并存储起来:

T=[t00t01t10t11]T = \begin{bmatrix} t_{00} & t_{01} \\ t_{10} & t_{11} \end{bmatrix}

然后再开始处理矩阵 LL 的各列。

评论