| 操作 | 乘法 | 加法 | 总FLOPs |
|---|---|---|---|
| GEMM M×K×K×N | MKN | MKN | 2MKN |
| 4×4块 | 64 | 48 | 128 |
输出驻留4×4阵列:每个PE持续累加一个输出元素,A水平传递,B垂直传递,MAC每个周期执行一次。
矩阵乘法C=A×B的三种数据复用模式各有优劣,选择取决于网络类型和硬件约束:
每个PE固定计算一个输出元素C[i][j],A的行和B的列流过PE:
权重W[i][j]预加载到PE(i,j),激活和部分和流过:
A的一行驻留在PE行中,B的列和部分和对流:
当矩阵大于阵列尺寸时,必须分块计算。分块策略直接影响数据复用效率:
| 分块方式 | 权重读次数 | 激活读次数 | 结果写次数 | 总访存 |
|---|---|---|---|---|
| 沿K分块 | M×K | K×N×⌈M/N⌉ | M×N×⌈K/N⌉ | 较高 |
| 沿M分块 | M×K×⌈N/N⌉ | K×N | M×N | 中等 |
| 沿N分块 | M×K | K×N | M×N | 最优 |
| PE类型 | 乘法器 | 累加器 | 寄存器 | 面积(门) | 功耗(mW@200MHz) |
|---|---|---|---|---|---|
| INT8 OS | 8×8→16 | 32-bit | 2×8 | ~600 | ~0.1 |
| INT8 WS | 8×8→16 | 32-bit | 3×8 | ~700 | ~0.12 |
| INT16 OS | 16×16→32 | 40-bit | 2×16 | ~2500 | ~0.3 |
| FP16 MAC | FP16 mul | FP32 | 2×16 | ~8000 | ~0.8 |
完成本课后,你已掌握 矩阵乘法阵列 的核心概念与硬件实现方法。
从4×4阵列扩展到实用规模需要解决多个工程问题:
当阵列规模增大时,时钟信号传输延迟成为瓶颈:
// 时钟树设计策略:
//
// 小阵列(≤8×8):
// 单时钟域,全局时钟树
// 时钟偏斜 < 50ps,满足时序
//
// 中等阵列(16×16):
// 时钟网格(Clock Mesh) + 局部缓冲
// 需要仔细平衡时钟偏斜
//
// 大阵列(32×32+):
// 分区时钟,每个区域独立时钟树
// 区域间用FIFO缓冲跨时钟域数据
// 或者使用流水线切割:每N行插入寄存器
矩阵乘法需要高效的广播/传递网络:
| 网络类型 | 延迟 | 面积 | 功耗 | 适用 |
|---|---|---|---|---|
| 全局广播总线 | 1 cycle | 高(N×走线) | 高 | 小阵列 |
| 树形广播 | log(N) cycle | 中 | 中 | 中等阵列 |
| 传递链(脉动) | N cycle | 低(局部走线) | 低 | 大阵列 |
除了硬件并行,数学优化也能减少计算量:
递归应用,N×N矩阵乘法复杂度从O(N²·⁸⁰⁷)降到O(N²·⁸⁰⁷)。但常数项大,仅对大矩阵(>128×128)有效。
// 分块矩阵乘法伪代码:
// for i in range(0, M, BLOCK):
// for j in range(0, N, BLOCK):
// for k in range(0, K, BLOCK):
// // 这个分块完全在片上SRAM中完成
// C[i:i+B][j:j+B] += A[i:i+B][k:k+B] × B[k:k+B][j:j+B]
//
// 关键:选择BLOCK大小使三个分块同时放入SRAM
// BLOCK_size = sqrt(SRAM_capacity / 3)
结合本课知识,一个实用的GEMM引擎包含以下组件:
| 组件 | 功能 | 面积占比 | 设计要点 |
|---|---|---|---|
| PE阵列 | 矩阵乘法 | 40% | INT8×INT8+INT32累加 |
| 权重Buffer | 存储当前块权重 | 25% | 双缓冲,DMA预取 |
| 激活Buffer | 存储当前块激活 | 20% | 双缓冲,IM2COL |
| 输出Buffer | 存储结果+部分和 | 10% | 支持部分和累加 |
| 控制器 | 分块调度 | 5% | FSM+计数器 |
本节展示如何用4×4阵列计算任意大小的矩阵乘法:
// 4×4阵列计算M×K × K×N矩阵:
// for tm in range(0, M, 4): // 遍历A的行块
// for tn in range(0, N, 4): // 遍历B的列块
// C[tm:tm+4][tn:tn+4] = 0 // 清零输出块
// for tk in range(0, K, 4): // 遍历K维度
// // 加载A[tm:tm+4][tk:tk+4]到输入Buffer
// // 加载B[tk:tk+4][tn:tn+4]到权重Buffer
// // 阵列计算4×4块乘法
// // 累加部分和到C[tm:tm+4][tn:tn+4]
// C[tm:tm+4][tn:tn+4] += A_block × B_block
//
// 总块数 = ceil(M/4) × ceil(N/4) × ceil(K/4)
// 每块需要:4×4=16 cycles计算 + 开销
| 矩阵大小 | 分块数 | A数据重用 | B数据重用 | 总DRAM访问 |
|---|---|---|---|---|
| 16×16 × 16×16 | 4×4×4=64 | 4× | 4× | 2×16²×16=8K |
| 64×64 × 64×64 | 16³=4096 | 16× | 16× | 2×64²×64=512K |
| 256×256 × 256×256 | 64³=262144 | 64× | 64× | 2×256²×256=32M |
分块策略的优化目标是最小化DRAM访问次数。沿K维度的部分和累加在片上完成,无需写回外存。
// 输出驻留PE间的数据传递:
// A数据:从左到右水平传递
// PE(i,j).a_out → PE(i,j+1).a_in
// 使用寄存器流水:always_ff @(posedge clk) a_out <= a_in;
//
// B数据:从上到下垂直传递
// PE(i,j).b_out → PE(i+1,j).b_in
// 使用寄存器流水:always_ff @(posedge clk) b_out <= b_in;
//
// 部分和:驻留在PE内部,不传递
// PE(i,j).acc <= PE(i,j).acc + a_in * b_in;
//
// 连线资源:
// 水平:4条16-bit线 × 4行 = 256 bit
// 垂直:4条16-bit线 × 4列 = 256 bit
// 总互连 = 512 bit (可管理)
精确计算矩阵乘法的存储需求是系统设计的基础:
对于M×K × K×N矩阵乘法在4×4阵列上分块计算:
| SRAM容量 | 分块大小 | A重用 | B重用 | 带宽节省 |
|---|---|---|---|---|
| 1KB | 2×2 | 2× | 2× | 50% |
| 4KB | 4×4 | 4× | 4× | 75% |
| 16KB | 8×8 | 8× | 8× | 87.5% |
| 64KB | 16×16 | 16× | 16× | 93.75% |
| 256KB | 32×32 | 32× | 32× | 96.9% |
SRAM每增大4倍,带宽节省增加约一半。64KB是典型的成本效益平衡点。
// 以ResNet-50第一层为例:
// Conv2D(3→64, 7×7, stride=2, input=224×224)
// 展开为矩阵乘法:64×147 × 147×12544
//
// 总MAC = 64 × 147 × 12544 = 117.6M
// @200MHz 16PE = 117.6M / 3.2G = 36.7ms
//
// 存储需求:
// A(权重): 64×147×2B = 18.8KB → 可完全放入SRAM
// B(输入): 147×12544×2B = 3.7MB → 需要分块
// C(输出): 64×12544×2B = 1.6MB → 需要分块写回
//
// 带宽需求:B的3.7MB必须从DRAM读取
// @200MHz需要:3.7MB / 36.7ms = 100MB/s
// DDR4-2400可提供:~19GB/s → 带宽充足
| 概念 | 定义 | 硬件影响 |
|---|---|---|
| 计算密度 | FLOPs/访存字节数 | 决定计算/带宽受限 |
| 数据复用 | 同一数据被多个PE使用 | 减少DRAM访问次数 |
| 流水线 | 多级操作重叠执行 | 提高吞吐量,增加延迟 |
| 双缓冲 | 两组缓冲区交替使用 | 隐藏数据加载延迟 |
| 分块计算 | 大矩阵分成小块处理 | 适配有限片上存储 |
| 饱和算术 | 溢出时钳位到极值 | 防止精度崩溃 |
| 时钟门控 | 空闲模块关闭时钟 | 降低动态功耗 |