卷积运算的硬件映射:从算法到电路
大家好,我是做芯片加速的老工程师了。今天咱们聊聊卷积运算怎么映射到硬件上。说实话,这问题我入行头三年都没完全搞明白,直到亲手调过几个加速器才真正吃透。
卷积在深度学习里太常见了。但软件里跑得飞快的卷积,到了硬件上就各种水土不服。为什么?因为卷积本质上是数据复用和计算模式的问题。你想想看,一个3x3的卷积核滑过特征图,每个像素要被用好几次,怎么安排数据流才能让计算单元不闲着?这就是硬件映射要解决的核心。
im2col + GEMM:最经典的映射方法
这个方法说白了就是「把卷积变成矩阵乘法」。我第一次看到这个思路时,觉得这招太聪明了——矩阵乘法在硬件上有多成熟啊,GEMM(通用矩阵乘法)的优化都做了几十年了。
具体怎么做?
假设输入是 HxWxC 的特征图,卷积核是 KxKxC,输出通道是 M。im2col 会把每个卷积窗口拉成一个列向量,所有窗口拼起来形成一个矩阵。这个矩阵的维度是 (K*K*C) x (H_out*W_out)。
然后呢?把卷积核也拉成矩阵,维度是 M x (K*K*C)。两个矩阵一乘,就得到输出。
// 伪代码示意
// 输入特征图: [H][W][C]
// 卷积核: [M][C][K][K]
// im2col 展开
for (int h = 0; h < H_out; h++) {
for (int w = 0; w < W_out; w++) {
for (int c = 0; c < C; c++) {
for (int kh = 0; kh < K; kh++) {
for (int kw = 0; kw < K; kw++) {
col_matrix[c*K*K + kh*K + kw][h*W_out + w] =
input[h*stride + kh][w*stride + kw][c];
}
}
}
}
}
// GEMM 计算
output[M][H_out*W_out] = weight[M][K*K*C] * col_matrix[K*K*C][H_out*W_out];
优点很明显:
- 复用成熟的矩阵乘法加速器,比如 systolic array
- 数据排布规整,访存模式简单
- 容易做并行化,M、C、H_out*W_out三个维度都能拆
缺点也致命:
- 内存爆炸!im2col 展开后数据量膨胀 K*K 倍。我做过一个 3x3 卷积,输入 224x224x64,展开后光存这个矩阵就要几百 MB
- 数据搬移开销大。展开操作本身就要大量访存
- 对 1x1 卷积还行,大核卷积就扛不住了
直接卷积的硬件实现
直接卷积,就是老老实实按卷积定义来算。每个输出像素,累加 K*K*C 次乘加。听起来简单,但硬件实现起来门道很多。
核心思路:设计一个乘加树,每个时钟周期处理一个卷积窗口的部分计算。我习惯把乘加树组织成 M 行 x (K*K*C) 列 的阵列,每行对应一个输出通道。
数据流设计是关键。我见过三种主流方案:
- 权重固定:权重存在寄存器里不动,输入数据流进来。适合权重小、输入大的场景
- 输入固定:输入数据不动,权重轮流加载。适合输入小、权重多的场景
- 输出固定:部分和存在本地,数据流式处理。这是我最常用的方案
避坑指南:我曾经在一个项目里用了「输入固定」的方案,结果发现卷积核数量一多,权重加载带宽根本跟不上。后来改成「输出固定」,把部分和存在片上 SRAM 里,带宽问题才解决。所以设计前一定要算清楚:你的瓶颈在计算还是访存?
直接卷积的好处是内存占用小,不需要展开。但控制逻辑复杂,尤其是处理 padding、stride、dilation 这些参数时,地址生成器要写不少代码。
Winograd 快速卷积算法
Winograd 是个数学技巧。它用更少的乘法次数完成卷积。对于 3x3 卷积,标准方法需要 9 次乘法,Winograd F(2,3) 只需要 4 次。省了一半多!
原理简述:把输入和卷积核都变换到另一个空间,在那个空间做 element-wise 乘法,再变换回来。变换矩阵是固定的,可以提前算好。
// Winograd F(2,3) 变换矩阵
// 输入变换: A^T * ((G * w * G^T) ⊙ (B^T * d * B)) * A
// 其中:
// B^T = [1 0 -1 0; 0 1 1 0; 0 -1 1 0; 0 1 0 -1]
// G = [1 0 0; 0.5 0.5 0.5; 0.5 -0.5 0.5; 0 0 1]
// A^T = [1 1 1 0; 0 1 -1 -1]
硬件实现要点:
- 变换矩阵的系数是常数,可以用硬连线实现
- 变换过程本身也有计算开销,要权衡
- 适合小卷积核(3x3、5x5),大核收益不明显
三种方法的对比
| 方法 | 计算效率 | 内存开销 | 控制复杂度 | 适用场景 |
|---|---|---|---|---|
| im2col + GEMM | 高(矩阵乘法优化成熟) | 高(数据膨胀 K² 倍) | 低(GEMM 核简单) | 大 batch、1x1 卷积 |
| 直接卷积 | 中等(受限于数据复用) | 低(无额外存储) | 高(地址生成复杂) | 实时推理、任意卷积 |
| Winograd | 最高(乘法次数少) | 中等(变换矩阵) | 中等(变换逻辑) | 小卷积核、高精度 |
我个人建议:不要死磕一种方法。实际芯片里往往是混合使用。比如我做过的一个加速器,1x1 卷积走 im2col+GEMM,3x3 卷积走 Winograd,其他走直接卷积。调度器根据卷积参数自动选择最优路径。
知识体系总览
下面这张图是我自己画的,把三种映射方法的核心逻辑串起来了。你看一眼就能明白它们之间的关系。
嗯,今天就先聊到这儿。这三种方法各有各的脾气,选哪个取决于你的芯片目标——要吞吐量还是要面积?要灵活还是要效率?没有银弹,只有权衡。
可可精品资料库