阶段一:计算基础

5课:脉动阵列控制器

📖 课程概述

脉动阵列效率取决于数据馈送——本课实现完整控制器,处理分块调度、权重加载、数据流控制和结果回写,将阵列从计算核心提升为计算引擎。

🔍 分块(Tiling)策略

C[M×N] = Σ A[M×K_t] × B[K_t×N]
循环软件硬件映射
外层Mfor m in 0..M/Ntile_m计数器
中层Nfor n in 0..N/Ntile_n计数器
内层Kfor k in 0..Kkcnt+脉动流

💻 6态FSM控制器

IDLE→LOADW(权重预加载)→COMP(K维迭代)→DRAIN(排空流水线)→STORE(回写结果)→NEXT(下一块)→DONE。双缓冲隐藏加载延迟。

📈 性能分析

总周期≈MKN/N² + 开销。8×8阵列上512×512矩阵≈262K周期@200MHz≈1.3ms。

🧪 练习

1) 添加双缓冲支持 2) 计算512³矩阵周期数 3) 添加阵列利用率计数器

🔬 双缓冲流水线详解

双缓冲(Double Buffering)是隐藏数据加载延迟的核心技术:

时序对比

周期无双缓冲有双缓冲
1-K加载Tile0加载Tile0
K+1-2K计算Tile0计算Tile0 + 加载Tile1
2K+1-3K加载Tile1计算Tile1 + 加载Tile2
3K+1-4K计算Tile1计算Tile2 + 加载Tile3
无双缓冲总时间 = N_tiles × (T_load + T_compute)
有双缓冲总时间 = T_load + N_tiles × max(T_load, T_compute)

当计算时间≥加载时间时,加载延迟被完全隐藏!

📐 分块调度策略对比

不同分块调度顺序影响数据复用和中间结果大小:

沿K维度分块(推荐)

沿M维度分块

沿N维度分块

💡 地址生成优化

高效的地址生成器(AGU)对性能至关重要:

数据类型访问模式AGU复杂度
权重(行主序)线性递增简单计数器
激活(IM2COL)2D滑动窗口行列计数器+偏移
部分和块状写入基地址+偏移
稀疏权重(CSR)间接寻址指针追踪+行列索引
🏆

成就解锁:脉动阵列控制器

完成本课后,你已掌握 脉动阵列控制器 的核心概念与硬件实现方法。

💻 Verilog实现

// 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
✅ Verilator验证通过 — 模块结构完整,逻辑正确,代码规范。

🔬 控制器的完整设计细节

一个实用的脉动阵列控制器需要处理多种边界情况:

非对齐矩阵的处理

当矩阵维度不是阵列大小的整数倍时,需要处理边缘块:

// 非对齐处理策略:
// 
// 示例: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设计是整个系统的核心,需要仔细处理每个状态的转换条件:

详细状态转换表

当前状态条件下一状态动作
IDLEcmd_start=1LOAD_W初始化计数器
LOAD_Wrow_cnt≥N²-1COMPUTE设置sa_wl=0
COMPUTEk_cnt≥K-1DRAIN设置sa_en=1
DRAINdrain_cnt≥N-1STORE读取psum
STOREcol_cnt≥N-1NEXT_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访问次数
流水线多级操作重叠执行提高吞吐量,增加延迟
双缓冲两组缓冲区交替使用隐藏数据加载延迟
分块计算大矩阵分成小块处理适配有限片上存储
饱和算术溢出时钳位到极值防止精度崩溃
时钟门控空闲模块关闭时钟降低动态功耗

🎯 学习目标检查

📚 拓展阅读

下一课预告:本课掌握的知识将在下一课中进一步深化。建议先完成练习,确保理解核心概念后再继续。