资讯动态

卷积操作与矩阵运算:从im2col到GEMM的加速原理与工程实践

发布时间:2026/10/14 12:32:42 来源:尧图企业网站定制
1. 切入点为什么我盯着卷积的矩阵运算看了三天前阵子我在调一个轻量化图像分类模型同样的网络结构在框架A上跑推理只要12毫秒换到框架B却要19毫秒。同一套权重同一个输入差距却接近一倍。排查到最后问题出在卷积层落到底层时用了不同的矩阵运算策略。各种卷积操作及其矩阵运算这个主题听起来像教科书里才有的内容但实际上它直接决定了你的模型在GPU、NPU、端侧芯片上到底能跑多快。如果你做模型压缩、剪枝量化、算子移植或者只是想让自己的网络结构更高效理解这一层就特别有价值。我当初在实训项目里看到im2col和GEMM这两个词时第一反应也是“这跟我写PyTorch有什么关系”直到自己动手拆过一次算子才真正意识到它们才是卷积加速的地基。这篇文章就把我踩过的坑、验证过的方法和现场笔记一起整理出来。全文不会跟你讨论太高深的理论只会把两类事情讲清楚第一类是工作中常见的各种卷积变体各自是干什么的、结构怎么理解第二类是把这些卷积操作映射到矩阵乘法上到底发生了什么事以及这种映射为什么能带来几倍甚至几十倍的性能差异。中间穿插我实际调参、验证、踩坑的记录能直接拿去参考。2. 先把卷积家族认全六种你在项目中一定会遇到的卷积2.1 标准卷积一切变体的基准线标准卷积是理解所有卷积操作的地基。输入是一张特征图形状用C_in通道数、H高、W宽表示卷积核是K×K的小窗口个数是C_out步长stride、填充padding这两个参数共同决定了输出尺寸。我习惯把标准卷积理解成“滑窗加权求和”权重在输入上按固定步长滑动每次覆盖一个小邻域把窗口内的数据与卷积核对应位置相乘再累加得到一个输出像素。对整张图做完就得到一张输出特征图。在一个实际项目中处理的是224×224×3的输入第一层用了32个3×3卷积核stride为2padding为1输出尺寸是112×112×32。当时我为了验证自己有没有理解错手动算了一个点的输出值找到输入上对应3×3邻域的27个数值3通道×9个位置分别乘上卷积核的27个权重求和后加上偏置就得到了输出特征图上那一个点的值。这一步算完标准卷积的机制就基本熟了。这个计算过程看起来平凡但它隐含了一个重要事实每个输出点都要做C_in×K×K次乘加运算。3×3卷积且输入输出通道都是256时一个输出点就是2304次乘加一张特征图有几十万个点运算量自然就上去了。这也是为什么在后面各种优化手段里减少K或减少通道数往往比减少层数收益更直接。2.2 分组卷积与深度可分离卷积通道间做文章分组卷积最早大家熟悉是因为某一著名网络结构用它解决了显存不够的问题但它真正的价值在于改变了通道之间的连接方式。标准卷积里每个输出通道会和全部输入通道做卷积分组卷积把输入通道和输出通道都分成G组每组内部独立做标准卷积组与组之间不通信。这样做的结果是计算量直接除以G参数量也除以G。我当时在某个移动端项目里把标准的3×3卷积改成group4的分组卷积在精度损失不到0.2个点的情况下单层FLOPs降了接近四倍模型文件大小也跟着明显下降。深度可分离卷积则是分组卷积的极致形态先把分组数设为输入通道数每个通道单独卷这叫深度卷积然后用1×1卷积去混合各通道的信息这叫逐点卷积。我第一次理解它的代码落地时发现原来一个普通卷积层被拆成了两个算子深度卷积负责空间特征提取逐点卷积负责跨通道特征融合。这个拆法在MobileNet系列里被用到了极致压缩比很可观。实际写代码时我会重点确认两件事深度卷积是否真的用了groupC_in逐点卷积是否kernel1这两个条件缺一个整个结构的效果和性能都会跑偏。2.3 空洞卷积、转置卷积与1×1卷积感受野与分辨率的不同解法空洞卷积解决的是一个矛盾想扩大感受野但又不想增加卷积核大小和计算量。做法是在卷积核的相邻元素之间插入空洞。我当时在语义分割项目里要把3×3卷积的感受野扩到接近5×5甚至7×7就用dilation2或3。需要提醒的是空洞卷积的输出尺寸计算公式里实际等效卷积核尺寸是K(K−1)×(dilation−1)不是直接填个数字就行写算子的时候非常容易算错。转置卷积经常被误叫成“反卷积”。它干的事和标准卷积正好反过来把小尺寸特征图上采样成大尺寸特征图。它的实现并不像名字暗示的那样是“卷积的逆运算”本质上仍然是一个卷积操作只是前向和反向传播的梯度流动方向反过来了。我在做图像生成网络时上采样路径里就用转置卷积替代插值视觉效果确实更平滑但训练也更容易出现棋盘伪影。一个关键的避坑经验是转置卷积的kernel_size要能被stride整除否则输出图会有不对称的重叠导致质量下降。1×1卷积看起来最简单却是性价比最高的通道变换工具。它的卷积核在空间上不跨区域只做通道维度的线性组合所以本质上是“通道间的全连接”。它最常用的三个场景是降维减少后续计算量、升维增加表达力、以及调整通道数以拼接不同分支的特征。在我的实际经验里1×1卷积配上ReLU在残差结构里用基本不会引入太多额外开销但能让网络的非线性更强这一点在深层模型里很划算。卷积类型核心作用主要代价典型场景标准卷积空间通道联合特征提取计算量大通用主干网络分组卷积通道分组并行处理组间信息割裂高分辨率输入、轻量化深度可分离卷积先逐通道再1×1混合表达力略降移动端分类检测空洞卷积扩大感受野不增参数易产生网格效应分割、检测转置卷积上采样恢复分辨率棋盘伪影风险生成模型1×1卷积通道组合调整无空间建模降维、通道变换3. 核心拆解卷积怎么变成矩阵乘法以及这步为什么值钱3.1 im2col把滑窗过程展开成矩阵的“摊大饼”操作标准卷积最费时的部分就是滑窗后的小邻域乘法。GPU的优势不在单点计算快而在大规模并行所以要尽可能把卷积转化成统一的大矩阵乘法。im2col就是完成这个过程的关键操作。它的名字是image to column意思就是把图像上的每个卷积窗口拉成一列。实际做法是对每个输出位置把对应输入窗口内的所有数值包含所有通道取出来排成一个列向量。窗口滑过整个输入之后所有这些列向量拼在一起就构成了一个巨大的矩阵。我拿一个最简单的例子推演过输入是1×4×4的单通道图卷积核是3×3stride1padding0输出尺寸是2×2。im2col后得到的输入矩阵形状是9×4其中每一列代表一个滑窗位置展开得到的9个数值。这4列分别对应输出图上4个像素位置。如果输入通道更多这个矩阵的高度继续累加每一列会包含所有通道在当前窗口里的值。这个展开操作在库里一般都有高度优化过的实现PyTorch里是unfoldTensorFlow/Keras里是extract_patches。自己手写的时候最需要注意的是步长和扩张率与窗口采样的对应关系我最初写错就是把扩张率当步长用结果展开出的窗口位置全偏了训练损失直接不收敛。展开得到的矩阵记为X_col卷积核也做同样的重排每个输出通道的卷积核被展开成一个行向量所有输出通道的行向量拼成权重矩阵W形状是C_out×(C_in×K×K)。这一步做完卷积运算就变成了非常标准的矩阵乘法。3.2 GEMM高性能矩阵乘法是怎么把速度提上去的GEMM是general matrix multiply的缩写也就是通用矩阵乘法。卷积经过im2col之后核心计算变成YW×X_col。这个矩阵乘法的维度往往非常大比如输入是112×112×128用3×3卷积输出256通道X_col大约是1152行×12544列W是256×1152一次乘法的累加次数就非常可观。如果不做任何优化三层循环暴力计算速度会很差。高性能GEMM的优化基本围绕三个方向减少内存访问、增加数据复用、利用SIMD指令做向量化。具体到实现上常见的手段包括分块tiling把大矩阵切小块让小块尽量塞进高速缓存重排内存布局把数据排成连续存放的格式保证读取时能命中缓存行还有一个是在最内层用向量化指令一次算多个乘加。我曾在CPU上做过一个简单实验同样的矩阵乘法用三层循环直写耗时2.4秒加了分块和向量化之后降到0.18秒差距超过13倍。这块是卷积矩阵化最值钱的地方。另一个关键点是算子融合。矩阵乘法之后通常紧跟一个偏置相加和ReLU激活。如果框架能把这些操作融合到同一个kernel里那就不需要把中间结果写回内存再读出来省掉一整轮访存开销。实际做推理优化时我见过最明显的一次提速就是靠融合这些相邻算子获得的。这也是现在各个推理引擎都在做算子融合的根本原因。3.3 内存换速度的代价im2col为什么吃内存以及怎么缓解im2col并不是免费的午餐它最直接的代价是内存膨胀。还是拿112×112×128输入、3×3卷积、输出256通道的例子来说X_col矩阵元素个数为1152×12544约1447万个数。按float32算差不多55MB。而原始输入特征图只有6.3MB左右内存膨胀接近9倍。在低端设备上这是不可接受的。业界常见的缓解思路有三个。第一个是只对局部区域做展开而不是整张图一次性im2col比如按行分批处理每次只展开几行窗口算完GEMM再释放把内存峰值降下来第二个是隐式im2col不真正在内存里展开矩阵而是在GEMM的读取阶段直接按窗口地址去取数精度和显式im2col完全一致但省掉了展开存储和拷贝的时间第三个是用im2col和Winograd折叠使用。Winograd是另一类变换方法对3×3卷积在特定条件下能把乘加次数降到原来的2.25倍左右但它对数值精度有一定影响我在精度要求高的模型里一般不会开。这三种方案怎么选要看部署目标。GPU上显存充足直接使用高展开度方案效果通常最好端侧芯片内存紧张我一般优先启用隐式方式。我的经验是与其被内存峰值卡住再改不如在设计网络时就把这块考虑进去尤其是通道数较大、特征图分辨率高的层提前估算展开矩阵大小。4. 不同卷积在矩阵视角下到底是怎么变化的4.1 深度可分离卷积拆分后的矩阵运算过程深度可分离卷积拆成两个算子后矩阵运算的形态变化很直观也很有启发性。先看深度卷积每个输入通道单独用一个二维卷积核处理输出通道数和输入通道数相同所以它的权重矩阵W_dw形状是C_in×(1×K×K)因为每个通道只有一份二维权重。对应的X_col形状是(K×K)×(H_out×W_out×C_in)。这里注意和标准卷积不同的是每一列不是所有通道的窗口数据拼接而是单个通道的窗口数据。运算时每个通道的输入只和自己的卷积核乘组与组之间没有交互。逐点卷积就是典型的1×1卷积卷积核空间尺寸为1所以权重矩阵W_pw形状是C_out×C_inX_col形状是C_in×(H_out×W_out)。这一步做的事就是纯通道维度的线性组合。把两段矩阵运算合起来看深度可分离卷积的计算量大概是标准卷积的1/C_out再加上1/K²左右通道数越大节省越明显。我在跑MobileNetV2结构的时候用这个拆分方式手动推演过一遍参数量发现确实每个标准卷积层拆完后参数量基本都降到原来的十分之一左右心中会对这个结构更有数。4.2 空洞卷积与转置卷积在展开时的不同取数逻辑空洞卷积在矩阵化时变化的点在于窗口内取数的间隔。im2col展开时需要跳过dilation−1个像素再取下一个点。这里的核心回归到我之前提过的等效卷积核尺寸。一个3×3卷积dilation2等效卷积核是5×5但有用的位置只有9个其余为空。做矩阵展开时要么直接按9个有效位置取数要么按5×5展开再乘上带有大量零值的权重。前者内存效率高后者实现简单但会平白浪费算力。我在端侧芯片上调算子时发现不少底层库默认按5×5展开速度比预期慢不少换成按9点展开后明显改善。所以如果你发现空洞卷积在某个平台上性能异常先查一下底层取数逻辑是不是按完整等效窗口做的。转置卷积的矩阵化则涉及一个特殊操作把输入特征图先做填充再执行标准卷积。具体来说stride为s时相当于在输入每两个相邻像素之间插入s−1行或列零然后再按步长为1做卷积。这个填充再卷积的过程用矩阵乘法视角看就是权重矩阵W的转置在起作用。标准卷积的前向是YW×X_col而转置卷积的前向可以写成Y_colWᵀ×X_col这里的W和X形状与标准卷积正好反过来。这也是转置卷积的名字来源不是数学意义上的逆运算而是梯度反向传播中使用的同一套转置操作。4.3 分组卷积与1×1卷积在矩阵化时的简化分组卷积矩阵化最简单把输入通道分成G组后每组独立做一次标准卷积的im2col与GEMM。整个计算过程就是在一个大矩阵乘法外套了一组小矩阵乘法。好处是每个小矩阵乘法规模变小了更容易被并行调度坏处是组间没有信息交互如果组数设得过大特征表达能力下降。我在一个轻量分类模型里试过group8和group4精度差了约0.5%但FLOPs只降了不到一半后来还是选回group4。1×1卷积矩阵化则有一个天然优势窗口尺寸为1不需要复杂的im2col展开直接把输入特征图当作矩阵X形状是C_in×(H×W)权重矩阵W形状是C_out×C_in一次GEMM就搞定。这也是1×1卷积在各种推理引擎中拥有极高效率的底层原因。我在做通道剪枝的时候特别喜欢拿1×1卷积作为切入点因为它没有空间维度通道重要性评估非常直接。可以说理解矩阵运算的视角越深对网络结构设计的判断就越准这也是我在几个项目中反复体会到的。5. 选型与配置什么时候用哪种卷积参数怎么设5.1 根据硬件平台和任务目标选卷积不同硬件平台对卷积类型的友好度完全不同。GPU上标准卷积和1×1卷积都很快因为cuDNN针对这两类做了深度优化NPU上深度可分离卷积效率高因为它避免了大量通道间访存CPU上分组卷积有时反而不好跑因为小矩阵乘法的线程并行度不如大矩阵乘法。我在一个端侧项目里就踩过这个坑模型为了轻量化用了很多深度可分离卷积在GPU上性能优秀部署到某款NPU上才发现深度卷积的底层实现效率不高跑起来比标准卷积还慢最后被迫改了一部分结构。所以选型的关键是先明确目标平台再决定卷积结构。做云端服务GPU充足优先标准卷积1×1卷积的组合精度好、优化成熟做移动端或边缘设备深度可分离卷积是主流但要提前验证目标芯片的算子库支持情况做实时性要求极高的任务1×1卷积或分组卷积往往更稳因为计算模式规整容易做极致优化。5.2 一组我用下来比较稳的参数配置以图像分类任务为例我在224×224输入、Cifar级别数据量上常用这套配置第一个卷积层用3×3、stride2、padding1输出64通道中间主干用残差结构每个残差块里第一个标准3×3卷积输出128通道第二个3×3卷积stride2做下采样最后接一个1×1卷积把通道升到256再接全局平均池化。整体FLOPs在2G左右最终精度和速度比较平衡。如果做语义分割我习惯用空洞卷积来替代部分下采样。具体做法是把主干网络的最后两个阶段的下采样去掉改用dilation2和dilation4的3×3卷积这样在不减少特征图分辨率的前提下扩大感受野分割细节保留效果更好。一个小细节空洞率不要连续都用一个数值最好梯度式增加我曾经把dilation从1直接跳到4训练时loss抖动明显改成1、2、4之后稳定多了。5.3 三个常见的配置陷阱第一个陷阱strided convolution和空洞卷积混用。strides1会让输出分辨率变小如果同时在同一个卷积里设置dilation1再用转置卷积去恢复很容易产生重叠伪影。我建议是把这两个动作分开下采样就单独用stride感受野扩展就单独用dilation。第二个陷阱深度可分离卷积之后通道数设置过小。因为逐点1×1卷积负责通道信息融合通道太少会严重损害表达力。我做过实验在同样FLOPs下把标准卷积替换成深度可分离卷积通道数从64提到96精度反而上升约0.8%。这背后的原因就是逐点卷积有足够维度去组合特征。第三个陷阱1×1卷积后面忘记加非线性激活。如果只是通道线性组合再叠加1×1多层1×1本质上等价于单个1×1卷积。所以用1×1卷积做通道变换时我习惯紧跟一个ReLU或GELU激活带来的非线性表达能力在深层网络里非常重要。6. 实际调试中遇到的坑排查方法和工具6.1 显存突然爆掉先怀疑im2col展开在某个训练任务里输入分辨率是512×512第一层卷积核设成7×7输出通道256。训练进行到第一个epoch直接显存溢出。我一开始以为模型太大后来算了一下才发现是im2col展开矩阵爆了。512×512分辨率、7×7卷积核、128通道输入展开后单是一层的中间矩阵就超过400MB再加上反向传播的驻留显存整卡肯定扛不住。解决方法是把卷积核改小、增加stride、降低输入分辨率或者让框架打开内存优化模式必要时做分块展开。这个案例给我的教训是显存问题不仅要看模型参数量还要看中间激活值的峰值。尤其网络前面几层处理高分辨率输入时卷积核稍大一点展开矩阵的体积就成倍增加。建议在拿到新模型后先做一次profile看每层激活值大小。6.2 输出特征图尺寸对不上多半是padding公式错了我有一段时间经常遇到网络在某个层直接维度报错。反复检查后问题大多出在我没算清楚padding对输出尺寸的影响。输出尺寸的公式是⌊(H2×padding−dilation×(K−1)−1)/stride1⌋。三个参数叠在一起很容易忽略dilation的作用。后来我养成了一个习惯写代码前先在纸上把输入尺寸、各参数都列出来算完输出尺寸再动手。尤其转置卷积的输出尺寸公式和标准卷积不一样更容易出错。6.3 推理速度比预期慢先查算子是否被优化当你把模型导出到推理引擎时同一个算子在不同框架下的执行路径可能完全不同。我做过的典型排查步骤是先逐层跑profile找出耗时最大的几个算子再查这些算子是否都走的是底层优化库里的专用路径比如cuDNN的卷积核是否被选中再看是否存在算子融合机会。有一次我发现一个模型速度慢原因是某个自定义的depthwise层没有被底层优化库识别回退到通用GEMM路径速度掉了五倍。修复方式是手动把层结构改成框架自带的标准算子速度立刻恢复正常。6.4 快速验证矩阵化结果正确的方法我常用一个简单方法验证自己写的im2col逻辑是否正确先构造一个很小的随机输入和固定卷积核手动按滑窗方式算出预期输出再分别用PyTorch的unfold和手动展开来实现对比结果是否一致。这里要注意浮点误差在1e-6范围内都算正常。如果手动实现的大矩阵乘法和框架自带卷积结果一致基本可以确定逻辑正确。顺带一提排查数值问题时用全为1的输入和固定权重会更容易算出预期值能更快定位是展开错了还是矩阵乘法错了。7. 写在最后的一点实际体会卷积操作和矩阵运算的关系最本质的一点是理解了这个映射你对模型的性能边界心里就有数了。我在动手写算子之前觉得网络结构设计、超参数调节充满玄学但真正沉下去把im2col和GEMM跑通后再看各种框架底层的优化手段思路会清晰很多。别急着背结论建议自己拿小矩阵推一遍输入4×4、卷积核3×3手写一次展开手算一次矩阵乘法再把结果交给框架验证。这个过程花不了半小时但带来的理解是看多少篇文章都替代不了的。还有一个我自己的小习惯在网络规模较大之前先用profile工具把每层的前向时间、激活值内存都打出来存好改结构时对比前后差异能省下大量排查时间。这种方式比拍脑袋调参可靠得多。希望这些内容对你有一点实际帮助。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑