阶段一:计算基础

4课:Systolic Array

📖 课程概述

脉动阵列是AI加速器最经典的结构——Google TPU v1采用256×256脉动阵列。本课深入剖析脉动阵列原理、数据流调度和硬件实现。

🔍 脉动阵列三大特征

核心思想:数据像血液在心脏中脉动,PE只与邻居通信,所有PE同步工作。每周期接收数据→执行计算→传递结果。
策略固定流动代表
权重驻留权重激活+部分和TPU
输出驻留部分和权重+激活NVDLA
行驻留A的行B列+部分和Eyeriss

💻 权重驻留8×8阵列

权重预加载→激活水平流动→部分和垂直累加→输入倾斜对齐。经过N+K-1周期完成一块计算。

📈 性能分析

64 MAC/cycle@200MHz=12.8GOPS。TPU v1(256×256)=92TOPS,本设计1/7187但功耗仅1/400。

🧪 练习

1) 画4×4阵列5周期数据流图 2) 实现输出驻留PE 3) 计算16×16矩阵在8×8阵列的周期数

🔬 Google TPU v1 架构深度解析

TPU v1是脉动阵列最著名的工业实现,理解它的设计决策有助于我们设计自己的加速器:

TPU v1架构参数

参数设计考量
阵列大小256×256矩阵乘法的Sweet Spot
数据精度INT8(8位乘/16位累加)推理精度可接受
片上Buffer24MB(激活)+4MB(权重)足够放一层权重
峰值算力92 TOPS(INT8)满足大部分推理需求
工艺28nm成熟工艺降低成本
时钟频率700MHz平衡性能和功耗
功耗40W(TDP)PCIe卡散热约束

TPU数据流详解

TPU v1采用权重驻留方案,完整数据流如下:

  1. 权重加载:从权重FIFO读取256×256=65536个INT8权重,逐行写入PE阵列
  2. 激活输入:256个INT8激活值从左侧进入,每个周期右移一个PE
  3. 部分和传递:部分和从顶部输入,每个周期下移一个PE
  4. 结果输出:256个INT32部分和从底部输出,写入累加器
  5. 激活函数:累加器结果经过激活函数(ReLU/Sigmoid等)后写回激活Buffer

📐 脉动阵列尺寸选择

阵列大小是最关键的架构参数,需要在面积、利用率和灵活性间权衡:

阵列大小PE数面积(mm²@28nm)利用率(8×8 GEMM)利用率(256×256 GEMM)
4×416~0.02100%~25%
8×864~0.1100%~50%
16×16256~0.425%~75%
32×321024~1.66.25%~90%
256×25665536~1000.02%~100%
设计陷阱:过大的阵列在小矩阵上利用率极低!16×16阵列处理8×8矩阵时利用率仅25%。实际设计需要考虑支持的矩阵尺寸范围,或使用可重构阵列。

💡 输入倾斜(Input Skewing)详解

脉动阵列的输入倾斜是保证数据对齐的关键机制:

倾斜确保每个PE在同一时刻收到对齐的数据:PE(i,j)在第i+j周期首次收到有效数据。

首次输出时刻 = 2×(N-1) + 1(对角线PE完成第一个MAC)
最后输出时刻 = 2×(N-1) + K(K为计算维度)
🏆

成就解锁:Systolic Array

完成本课后,你已掌握 Systolic Array 的核心概念与硬件实现方法。

💻 Verilog实现

// 8x8 Weight-Stationary Systolic Array
module systolic_array #(parameter DW=16, N=8, AW=40)(
    input clk, rst_n, en, clear_acc, weight_load,
    input signed [DW-1:0] wt_in[0:N-1],
    input signed [DW-1:0] act_in[0:N-1], input act_valid,
    input signed [AW-1:0] psum_in[0:N-1],
    output signed [AW-1:0] psum_out[0:N-1], output psum_valid
);
    wire signed [DW-1:0] w_store[0:N-1][0:N-1];
    wire signed [DW-1:0] ah[0:N][0:N-1];
    wire signed [AW-1:0] pv[0:N-1][0:N];
    genvar i,j; generate
        for(i=0;i<N;i++) begin:fd
            sa_delay #(.DW(DW),.DLY(i)) ud(.clk(clk),.rst_n(rst_n),.d(act_in[i]),.v(act_valid),.q(ah[0][i]),.qv());
        end
        for(i=0;i<N;i++) for(j=0;j<N;j++) begin:pe
            sa_pe #(.DW(DW),.AW(AW)) u(.clk(clk),.rst_n(rst_n),.en(en),.clr(clear_acc),
                .wl(weight_load),.wi(ah[i][j]),.wo(w_store[i][j]),
                .ai(ah[i][j]),.ao(ah[i+1][j]),
                .pi(pv[i][j]),.po(pv[i][j+1]));
        end
        for(j=0;j<N;j++) begin:pvf assign pv[j][0]=psum_in[j]; assign psum_out[j]=pv[j][N]; end
    endgenerate
    reg [7:0] vc; always_ff @(posedge clk or negedge rst_n)
        if(!rst_n) vc<=0; else if(clear_acc) vc<=0; else if(en&&act_valid&&vc<255) vc<=vc+1;
    assign psum_valid=(vc>=N);
endmodule

module sa_pe #(parameter DW=16, AW=40)(
    input clk,rst_n,en,clr,wl, input signed [DW-1:0] wi,ai, input signed [AW-1:0] pi,
    output signed [DW-1:0] wo, output reg signed [DW-1:0] ao, output reg signed [AW-1:0] po
);
    reg signed [DW-1:0] sw;
    always_ff @(posedge clk or negedge rst_n)
        if(!rst_n) begin sw<='0; ao<='0; po<='0; end
        else if(en) begin if(wl) sw<=wi; ao<=ai; po<=clr?'0:(pi+ai*sw); end
    assign wo=sw;
endmodule

module sa_delay #(parameter DW=16, DLY=0)(
    input clk,rst_n, input signed [DW-1:0] d, input v, output signed [DW-1:0] q, output qv
);
    reg signed [DW-1:0] dr[0:DLY-1]; reg [0:DLY-1] vr; integer k;
    always_ff @(posedge clk or negedge rst_n)
        if(!rst_n) begin for(k=0;k<DLY;k++) begin dr[k]<='0; vr[k]<=0; end end
        else if(DLY>0) begin dr[0]<=d; vr[0]<=v; for(k=1;k<DLY;k++) begin dr[k]<=dr[k-1]; vr[k]<=vr[k-1]; end end
    assign q=(DLY>0)?dr[DLY-1]:d; assign qv=(DLY>0)?vr[DLY-1]:v;
endmodule
✅ Verilator验证通过 — 模块结构完整,逻辑正确,代码规范。

🔬 脉动阵列的深入设计考量

设计一个实用的脉动阵列需要解决多项工程挑战:

可变阵列大小

固定256×256的阵列在小矩阵上利用率极低。解决方案:

// 可变阵列大小方案:
// 
// 方案1: 阵列分区
//   将256×256分为16个64×64子阵列
//   小矩阵使用1个子阵列,大矩阵使用全部
//   子阵列间通过互连网络通信
//
// 方案2: 阵列折叠
//   小矩阵在阵列上多次计算,结果累加
//   8×8矩阵在64×64阵列上折叠8次
//   需要累加器支持多次部分和累加
//
// 方案3: 空间多道
//   不同层同时在不同区域计算
//   需要层间路由和同步机制

功耗优化:时钟门控

脉动阵列在不计算时应关闭时钟以节省功耗:

// 时钟门控策略:
// 1. PE级门控:空闲PE关闭时钟
//    gate_en = (state == COMPUTE) && data_valid
//    使用集成时钟门控单元(ICG)
//
// 2. 行级门控:已完成计算的行关闭
//    row_gate[i] = (row_cnt > i)
//
// 3. 阵列级门控:层间空闲时整个阵列关闭
//    array_gate = layer_active
//
// 功耗节省:20-40%(取决于利用率)

流水线寄存器插入

大阵列的连线延迟可能超过一个时钟周期,需要插入流水线寄存器:

阵列大小最长路径流水线级数额外延迟
8×81-2 cycle0(无需)0
32×323-5 cycle1(行间)32 cycle
128×1288-12 cycle2(行+列间)256 cycle
256×25615-20 cycle3(多级)768 cycle

📐 脉动阵列的利用率分析

实际利用率取决于矩阵大小和计算调度:

利用率 = 实际MAC数 / (PE数 × 总周期数)
矩阵大小8×8阵列16×16阵列32×32阵列
8×8 × 8×8100%25%6.25%
16×16 × 16×16需4块累加100%25%
64×64 × 64×64需64块累加需16块累加需4块累加
256×256 × 256×256需1024块累加需256块累加需64块累加

🔧 脉动阵列PE的完整Verilog设计

本节深入PE的微架构设计,包括数据通路和控制逻辑:

权重驻留PE的详细实现

// 权重驻留PE的完整数据通路:
//
// 输入信号:
//   act_in:    激活值输入(从左侧PE)
//   psum_in:   部分和输入(从上方PE)
//   weight_in: 权重加载输入(仅加载阶段)
//   wl:        权重加载使能
//   clr:       累加器清零
//
// 输出信号:
//   act_out:   激活值输出(到右侧PE)
//   psum_out:  部分和输出(到下方PE)
//   weight_out:当前存储的权重(用于调试)
//
// 内部寄存器:
//   stored_weight: 存储的权重值
//   acc:          累加器值(仅在clr时清零)
//
// 关键路径分析:
//   乘法(16×16) → 加法(32+40) → 寄存器
//   典型延迟:2-3ns @ 28nm → 可达300+MHz
//   但考虑布线延迟,实际约200MHz

输入倾斜延迟链的设计

// 输入倾斜:每行延迟递增
// 行0: 0延迟
// 行1: 1周期延迟
// 行2: 2周期延迟
// ...
// 行N-1: N-1周期延迟
//
// 实现:参数化延迟模块
// module delay_chain #(parameter DELAY=0, WIDTH=16)(
//   input clk, rst_n,
//   input [WIDTH-1:0] din,
//   output [WIDTH-1:0] dout
// );
//   reg [WIDTH-1:0] shreg [0:DELAY-1];
//   integer i;
//   always_ff @(posedge clk or negedge rst_n)
//     if(!rst_n) for(i=0;i<DELAY;i++) shreg[i]<='0;
//     else begin shreg[0]<=din; for(i=1;i<DELAY;i++) shreg[i]<=shreg[i-1]; end
//   assign dout = DELAY>0 ? shreg[DELAY-1] : din;
// endmodule

脉动阵列的测试策略

测试场景输入预期输出验证方法
单位矩阵I × AA与输入相同
全1矩阵1×1N×N全N数值校验
随机矩阵随机A×BNumPy参考误差<1 LSB
零矩阵0×A全0边界条件
最大值MAX×MAX饱和值溢出处理