| 策略 | 固定 | 流动 | 代表 |
|---|---|---|---|
| 权重驻留 | 权重 | 激活+部分和 | TPU |
| 输出驻留 | 部分和 | 权重+激活 | NVDLA |
| 行驻留 | A的行 | B列+部分和 | Eyeriss |
权重预加载→激活水平流动→部分和垂直累加→输入倾斜对齐。经过N+K-1周期完成一块计算。
TPU v1是脉动阵列最著名的工业实现,理解它的设计决策有助于我们设计自己的加速器:
| 参数 | 值 | 设计考量 |
|---|---|---|
| 阵列大小 | 256×256 | 矩阵乘法的Sweet Spot |
| 数据精度 | INT8(8位乘/16位累加) | 推理精度可接受 |
| 片上Buffer | 24MB(激活)+4MB(权重) | 足够放一层权重 |
| 峰值算力 | 92 TOPS(INT8) | 满足大部分推理需求 |
| 工艺 | 28nm | 成熟工艺降低成本 |
| 时钟频率 | 700MHz | 平衡性能和功耗 |
| 功耗 | 40W(TDP) | PCIe卡散热约束 |
TPU v1采用权重驻留方案,完整数据流如下:
阵列大小是最关键的架构参数,需要在面积、利用率和灵活性间权衡:
| 阵列大小 | PE数 | 面积(mm²@28nm) | 利用率(8×8 GEMM) | 利用率(256×256 GEMM) |
|---|---|---|---|---|
| 4×4 | 16 | ~0.02 | 100% | ~25% |
| 8×8 | 64 | ~0.1 | 100% | ~50% |
| 16×16 | 256 | ~0.4 | 25% | ~75% |
| 32×32 | 1024 | ~1.6 | 6.25% | ~90% |
| 256×256 | 65536 | ~100 | 0.02% | ~100% |
脉动阵列的输入倾斜是保证数据对齐的关键机制:
倾斜确保每个PE在同一时刻收到对齐的数据:PE(i,j)在第i+j周期首次收到有效数据。
完成本课后,你已掌握 Systolic Array 的核心概念与硬件实现方法。
// 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设计一个实用的脉动阵列需要解决多项工程挑战:
固定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×8 | 1-2 cycle | 0(无需) | 0 |
| 32×32 | 3-5 cycle | 1(行间) | 32 cycle |
| 128×128 | 8-12 cycle | 2(行+列间) | 256 cycle |
| 256×256 | 15-20 cycle | 3(多级) | 768 cycle |
实际利用率取决于矩阵大小和计算调度:
| 矩阵大小 | 8×8阵列 | 16×16阵列 | 32×32阵列 |
|---|---|---|---|
| 8×8 × 8×8 | 100% | 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的微架构设计,包括数据通路和控制逻辑:
// 权重驻留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 × A | A | 与输入相同 |
| 全1矩阵 | 1×1 | N×N全N | 数值校验 |
| 随机矩阵 | 随机A×B | NumPy参考 | 误差<1 LSB |
| 零矩阵 | 0×A | 全0 | 边界条件 |
| 最大值 | MAX×MAX | 饱和值 | 溢出处理 |