| 循环 | 软件 | 硬件映射 |
|---|---|---|
| 外层M | for m in 0..M/N | tile_m计数器 |
| 中层N | for n in 0..N/N | tile_n计数器 |
| 内层K | for k in 0..K | kcnt+脉动流 |
IDLE→LOADW(权重预加载)→COMP(K维迭代)→DRAIN(排空流水线)→STORE(回写结果)→NEXT(下一块)→DONE。双缓冲隐藏加载延迟。
双缓冲(Double Buffering)是隐藏数据加载延迟的核心技术:
| 周期 | 无双缓冲 | 有双缓冲 |
|---|---|---|
| 1-K | 加载Tile0 | 加载Tile0 |
| K+1-2K | 计算Tile0 | 计算Tile0 + 加载Tile1 |
| 2K+1-3K | 加载Tile1 | 计算Tile1 + 加载Tile2 |
| 3K+1-4K | 计算Tile1 | 计算Tile2 + 加载Tile3 |
当计算时间≥加载时间时,加载延迟被完全隐藏!
不同分块调度顺序影响数据复用和中间结果大小:
高效的地址生成器(AGU)对性能至关重要:
| 数据类型 | 访问模式 | AGU复杂度 |
|---|---|---|
| 权重(行主序) | 线性递增 | 简单计数器 |
| 激活(IM2COL) | 2D滑动窗口 | 行列计数器+偏移 |
| 部分和 | 块状写入 | 基地址+偏移 |
| 稀疏权重(CSR) | 间接寻址 | 指针追踪+行列索引 |
完成本课后,你已掌握 脉动阵列控制器 的核心概念与硬件实现方法。
// Systolic Array Controller — Tile scheduling FSM
module systolic_ctrl #(parameter DW=16, N=8, AW=12)(
input clk, rst_n, cmd_start,
input [AW-1:0] m_sz, k_sz, n_sz,
output cmd_done, output [AW-1:0] status,
output reg [AW-1:0] a_addr, output reg a_en,
input signed [DW-1:0] a_rdata, input a_rvalid,
output reg [AW-1:0] b_addr, output reg b_en,
input signed [DW-1:0] b_rdata, input b_rvalid,
output reg [AW-1:0] c_addr, output reg c_en, output reg [DW*N-1:0] c_wdata,
output sa_en, sa_clr, sa_wl, output signed [DW-1:0] sa_act[0:N-1], output sa_av,
input [DW*N-1:0] sa_po, input sa_pv
);
localparam IDLE=0,LOADW=1,COMP=2,DRAIN=3,STORE=4,NEXT=5,DONE=6;
reg [3:0] st,nst; reg [AW-1:0] tm,tk,tn,row,kcnt; reg [7:0] dcnt;
always_ff @(posedge clk or negedge rst_n)
if(!rst_n) begin st<=IDLE; tm<='0; tk<='0; tn<='0; row<='0; kcnt<='0; dcnt<=0; a_addr<='0; b_addr<='0; c_addr<='0; a_en<=0; b_en<=0; c_en<=0; end
else begin st<=nst;
case(st) LOADW: if(a_rvalid) a_addr<=a_addr+1;
COMP: begin a_en<=(kcnt<k_sz); b_en<=(kcnt<k_sz); a_addr<=a_addr+1; b_addr<=b_addr+1; kcnt<=kcnt+1; end
DRAIN: dcnt<=dcnt+1;
STORE: begin c_en<=1; c_addr<=c_addr+1; c_wdata<=sa_po; end
NEXT: begin tn<=tn+N; if(tn+N>=n_sz) begin tn<='0; tm<=tm+N; end kcnt<='0; row<='0; end
endcase
end
always_comb begin nst=st; case(st)
IDLE: if(cmd_start) nst=LOADW; LOADW: if(row>=N*N-1) nst=COMP;
COMP: if(kcnt>=k_sz) nst=DRAIN; DRAIN: if(dcnt>=N) nst=STORE;
STORE: nst=NEXT; NEXT: nst=(tm>=m_sz&&tn>=n_sz)?DONE:LOADW; DONE: nst=IDLE;
endcase end
assign sa_en=(st==COMP)||(st==LOADW); assign sa_clr=(st==LOADW)&&(row==0);
assign sa_wl=(st==LOADW); assign sa_av=(st==COMP);
assign cmd_done=(st==DONE); assign status={12'd0,st};
genvar i; generate for(i=0;i<N;i++) assign sa_act[i]=a_rvalid?a_rdata:'0; endgenerate
endmodule一个实用的脉动阵列控制器需要处理多种边界情况:
当矩阵维度不是阵列大小的整数倍时,需要处理边缘块:
// 非对齐处理策略:
//
// 示例:100×100矩阵在8×8阵列上
// 100/8 = 12.5 → 需要13个分块(12满+1部分)
//
// 策略1: 零填充(Zero Padding)
// 将100填充到104,多出的4行/列用0填充
// 优点:硬件简单,阵列利用率100%
// 缺点:浪费4%的计算
//
// 策略2: 部分阵列
// 最后一个分块只使能部分PE
// 优点:不浪费计算
// 缺点:需要PE级使能控制,复杂
//
// 策略3: 结果掩码
// 全部计算但只取有效部分
// 优点:硬件最简单
// 缺点:浪费计算和功耗
逐层调度是最简单的,但可以更优化:
| 调度方式 | 描述 | 延迟 | 缓冲需求 | |
|---|---|---|---|---|
| 逐层顺序 | 每层独立完成 | Σ(T_layer_i) | 2个(IFM+OFM) | |
| 双缓冲流水 | 当前层计算+下层加载 | T_0+Σmax(T_load,T_comp) | 4个(双缓冲) | |
| 层间融合 | 多算子融合执行 | 最低 | 最少 | 需要分析依赖 |
性能计数器帮助分析阵列利用率和瓶颈:
// 关键性能指标:
// 1. 总周期数 cycle_total
// 2. 活跃PE周期数 pe_active_cycles (每个PE每周期+1)
// 3. DMA等待周期数 dma_stall_cycles
// 4. 计算等待周期数 comp_stall_cycles
//
// 利用率 = pe_active_cycles / (PE数 × cycle_total)
// DMA效率 = 1 - dma_stall_cycles / cycle_total
// 计算效率 = 1 - comp_stall_cycles / cycle_total
//
// 典型目标:利用率 > 70%,DMA效率 > 90%
控制器需要满足严格的时序约束:
控制器的FSM设计是整个系统的核心,需要仔细处理每个状态的转换条件:
| 当前状态 | 条件 | 下一状态 | 动作 |
|---|---|---|---|
| IDLE | cmd_start=1 | LOAD_W | 初始化计数器 |
| LOAD_W | row_cnt≥N²-1 | COMPUTE | 设置sa_wl=0 |
| COMPUTE | k_cnt≥K-1 | DRAIN | 设置sa_en=1 |
| DRAIN | drain_cnt≥N-1 | STORE | 读取psum |
| STORE | col_cnt≥N-1 | NEXT_TILE | 写回内存 |
| NEXT_TILE | 所有块完成 | DONE | 设置done=1 |
| NEXT_TILE | 还有块 | LOAD_W | 更新tile索引 |
| DONE | - | IDLE | 清done |
// A矩阵地址生成(行主序)
// a_addr = tile_m * K + k_cnt * 1 + inner_offset
// 其中inner_offset在PE间传递
//
// B矩阵地址生成(列主序)
// b_addr = tile_n * K + k_cnt * N + inner_offset
//
// 权重预加载地址:
// w_addr = (tile_m/ARRAY_N) * ARRAY_N² + pe_row * ARRAY_N + pe_col
//
// 结果写回地址:
// c_addr = tile_m * N + tile_n + row * N + col
// 验证checklist:
// 1. 空闲状态正确响应start信号
// 2. 权重加载:N²个权重正确写入PE
// 3. 计算:K个周期正确送入激活值
// 4. 排空:流水线中数据完整输出
// 5. 写回:结果写入正确地址
// 6. 分块:所有分块正确遍历
// 7. 完成:done信号正确产生
// 8. 连续推理:第二次推理正确启动
理解控制器时序对调试和优化至关重要:
// 8×8阵列计算32×32 × 32×32矩阵的时序:
//
// Cycle: 0 1 2 ... 64 65 66 ... 96 97 ... 128
// |---LOAD_W---|---COMPUTE---|--DRAIN--|--STORE--|
//
// LOAD_W: 写入64个权重到PE阵列
// sa_wl=1, sa_en=1
// a_read_addr: 0→63
// 每周期写入1个权重
//
// COMPUTE: 逐周期送入激活值
// sa_wl=0, sa_en=1
// a_read_addr: 逐行递增
// 每周期8个PE同时计算
// k_cnt: 0→31
//
// DRAIN: 排空流水线
// sa_en=0
// 等待8周期让数据流过
//
// STORE: 读回结果
// c_write_en=1
// 每周期写回8个结果
//
// 总周期 = 64 + 32 + 8 + 64 = 168 cycles/tile
// 4×4=16 tiles → 2688 cycles total
// @200MHz → 13.4μs
// 双缓冲下的时序(计算与加载重叠):
//
// Tile 0: [LOAD_W0][COMPUTE0][DRAIN0][STORE0]
// Tile 1: [LOAD_W1][COMPUTE1][DRAIN1][STORE1]
// Tile 2: [LOAD_W2][COMPUTE2]...
//
// LOAD_W1与COMPUTE0并行执行
// 节省时间 = min(LOAD_time, COMPUTE_time) × (N_tiles-1)
//
// 对于计算受限的场景(常见):
// 有效时间 ≈ T_load + N_tiles × T_compute
// 加速比 ≈ (T_load + T_compute) / max(T_load, T_compute)
| 概念 | 定义 | 硬件影响 |
|---|---|---|
| 计算密度 | FLOPs/访存字节数 | 决定计算/带宽受限 |
| 数据复用 | 同一数据被多个PE使用 | 减少DRAM访问次数 |
| 流水线 | 多级操作重叠执行 | 提高吞吐量,增加延迟 |
| 双缓冲 | 两组缓冲区交替使用 | 隐藏数据加载延迟 |
| 分块计算 | 大矩阵分成小块处理 | 适配有限片上存储 |
| 饱和算术 | 溢出时钳位到极值 | 防止精度崩溃 |
| 时钟门控 | 空闲模块关闭时钟 | 降低动态功耗 |