资讯动态

卷积神经网络推理内存三笔账:权重、特征图与临时缓冲

发布时间:2026/10/15 11:07:43 来源:尧图企业网站定制
1. 一个让很多人困惑的现象权重才几兆跑起来却吃掉几个G先把场景摆出来。你训练好一个卷积网络权重文件导出成 FP32 格式一看大小比如 3.5 MB、12 MB甚至更小。你心想这玩意儿这么轻部署到设备上肯定没压力。结果一跑起来内存占用直接飙到几百兆甚至上 G任务管理器或者nvidia-smi上的数字让你怀疑人生。这个反差是真实存在的而且非常普遍。我最早做移动端推理的时候也踩过这个坑模型文件 5 MBApp 一加载进程内存涨了 300 MB当时第一反应是是不是哪里内存泄漏了查了半天才发现根本不是泄漏是卷积这个操作本身在运行时需要的内存和权重文件大小完全是两码事。要理解这件事得把卷积在运行时的内存开销拆成三笔账来算第一笔权重本身占的内存也就是你看到的那个文件大小这部分通常最小第二笔特征图激活值占的内存这是真正的大头往往比权重高出一到两个数量级第三笔计算过程中的临时缓冲和中间结果包括 im2col 展开、卷积累加缓冲、算子融合前的中间张量等。很多人只算了第一笔账所以才会觉得模型这么小凭什么吃这么多内存。这篇就把这三笔账一笔一笔算清楚顺带把 MACs、参数量、FP32 这几个概念之间的关系捋顺让你以后看到任何一个卷积网络都能大致估出它的运行时内存量级。提示本文讨论的是推理inference阶段的内存占用。训练阶段还要额外存梯度、优化器状态、前向激活缓存内存需求通常是推理的数倍那是另一个话题。2. 第一笔账权重内存为什么它总是看起来很小2.1 参数量和文件大小的换算关系先把这个最基础的关系说清楚因为后面所有估算都建立在它之上。一个卷积层的参数量公式是参数量 卷积核高 × 卷积核宽 × 输入通道数 × 输出通道数 输出通道数偏置举个具体的例子。一个标准的 3×3 卷积输入 256 通道输出 256 通道参数量 3 × 3 × 256 × 256 256 589,824 256 ≈ 590K这大约 59 万个参数。如果用 FP32 存储每个参数 4 字节590K × 4 Byte ≈ 2.36 MB就这一层2.36 MB。一个典型的 ResNet-50 有大约 2550 万个参数FP32 下25.5M × 4 Byte ≈ 102 MB所以 ResNet-50 的权重文件大概就是 100 MB 左右这个数字大家应该都见过。而像 MobileNet 这种用深度可分离卷积的轻量网络参数量能压到 400 万左右FP32 下才 16 MB。这里就能看出第一个关键点权重内存 参数量 × 每参数字节数。FP32 是 4 字节FP16 是 2 字节INT8 是 1 字节。量化能把权重内存直接砍到 1/4 或 1/2这也是为什么端侧部署特别爱用量化。2.2 为什么权重往往不是内存瓶颈把权重和后面的特征图一比你就明白为什么它经常不是主角了。还是 ResNet-50权重 100 MB。但它第一层卷积的输入是一张 224×224×3 的图经过 7×7 步长 2 的卷积后变成 112×112×64 的特征图。这个特征图如果按 FP32 存112 × 112 × 64 × 4 Byte ≈ 3.2 MB单看一层不大但问题是网络是一层一层串起来的中间的特征图在推理时是需要保留的至少当前层和下一层要用。而且越往后通道数越多虽然空间尺寸在降但通道数在涨。到了 stage 4特征图可能是 14×14×204814 × 14 × 2048 × 4 Byte ≈ 1.6 MB单张特征图看着都不大但一个网络几十上百层如果框架不做内存复用把所有中间激活都留着累加起来就非常可观了。更别说 batch size 一上去直接乘以 batch。2.3 一个容易被忽略的点权重的内存布局还有一个细节权重在内存里不一定就是参数量 × 4 字节这么干净。很多推理框架为了计算效率会把权重重新排布成更适合 SIMD 或张量核心的格式比如把 OIHW 转成 NHWC 或者分块布局这个过程中可能产生额外的对齐填充。对齐到 16 字节、32 字节边界小通道数的层可能凭空多出不少。我实测过一个通道数很小的网络理论权重 8 MB实际加载后框架报告占用了 11 MB 左右多出来的就是布局转换和对齐的开销。这个比例在通道数是 3、8、16 这种小数字时会特别明显。3. 第二笔账特征图内存真正的大头在这里3.1 特征图内存的估算公式特征图也叫激活值、activation的内存是推理时内存占用的主要来源。它的估算公式很直接单层特征图内存 输出高 × 输出宽 × 输出通道数 × batch size × 每元素字节数注意这里有个 batch size这是很多人估算时漏掉的。你本地测试用 batch1觉得内存还行一上生产 batch32内存直接翻 32 倍然后就开始 OOM。拿一个具体的层算一下。假设某层输出是 56×56×256batch1FP3256 × 56 × 256 × 1 × 4 Byte 3,211,264 Byte ≈ 3.06 MB单层 3 MB。但一个网络里这种量级的层可能有十几二十个。如果框架不做任何复用全留着3 MB × 20 层 ≈ 60 MB这还只是 batch1。batch16 就是接近 1 GB。这就是为什么模型文件 100 MB跑起来吃 1 GB 内存完全说得通。3.2 为什么特征图不能像权重那样用完就扔有人会问既然中间特征图用完就没用了为什么不能立刻释放理论上可以而且现代推理框架确实在做这件事叫内存复用memory reuse / memory pooling。核心思路是分析整个计算图找出哪些张量的生命周期不重叠让它们共享同一块内存。但这里有个约束残差连接skip connection会拉长张量的生命周期。ResNet 里那个 shortcut意味着某一层的输出要一直留到后面和另一条分支相加中间隔了好几层都不能释放。这就导致内存复用的效率下降峰值内存被抬高。我做过一个对比实验同样深度的网络带残差连接的版本峰值内存比不带的高出大约 30% 到 40%原因就是残差把张量生命周期拉长了复用池没法充分回收。3.3 峰值内存才是决定你能不能跑起来的关键这里要强调一个概念决定 OOM 与否的不是平均内存而是峰值内存。推理过程中内存占用是动态变化的。某些时刻比如某个大特征图刚算出来、下一层还没开始消费它的时候内存占用会达到峰值。框架的内存池通常会预留比峰值略多一点的空间避免频繁向系统申请释放。所以你在监控里看到的内存占用往往是峰值附近的值而不是某一层的瞬时值。这也是为什么有时候你把 batch size 从 16 降到 15内存没怎么变——因为峰值是由某个特定层决定的降一点点 batch 没跨过那个台阶。4. 第三笔账临时缓冲与计算中间态最隐蔽的开销4.1 im2col把卷积变成矩阵乘的代价很多框架尤其是早期版本和某些 CPU 实现会把卷积转成矩阵乘法来加速用的方法叫im2col。原理是把输入特征图里每个卷积窗口覆盖的区域拉成一列拼成一个大矩阵然后和展开后的卷积核做矩阵乘。这个转换的代价是什么内存膨胀。假设输入是 H×W×C卷积核 K×K输出是 H×W×C。im2col 之后的矩阵大小是(K × K × C) × (H × W)对比原始输入 H×W×C膨胀倍数大约是 K×K考虑步长的话还要再乘。一个 3×3 卷积im2col 后输入数据膨胀 9 倍。如果输入特征图本身是 10 MB展开后就是 90 MB。这就是为什么有些框架在跑大卷积时会突然内存飙升——不是权重的问题是 im2col 这个中间矩阵撑起来的。4.2 现代框架怎么规避这个开销好消息是现在主流框架基本不用朴素的 im2col 了改用隐式 GEMMimplicit GEMM边算边取数据不显式展开那个大矩阵。还有 Winograd 算法通过变换减少乘法次数但代价是需要额外的变换缓冲。Winograd 的内存开销值得单独说一句。F(2×2, 3×3) 这种常见的 Winograd 配置需要把输入 tile 做变换变换后的数据比原始输入大而且每个 tile 都要单独处理。在通道数多的时候这个变换缓冲可能达到几十 MB。我见过一个案例某框架开启 Winograd 后速度提升明显但峰值内存涨了将近 50 MB在内存紧张的设备上反而得不偿失。4.3 算子融合能省下多少算子融合operator fusion是省内存的一大利器。典型的是 Conv BN ReLU 融合把三个算子合成一个中间结果不落内存。不融合的话Conv 输出要写一块内存BN 读它再写一块ReLU 再读再写。融合之后Conv 算完直接在寄存器或缓存里做 BN 和 ReLU只写一次最终结果。省下的就是中间那两块特征图的内存。对于大网络算子融合能省下的内存相当可观。我实测过一个 50 层左右的网络开启融合后峰值内存降了大约 25%。这个收益在移动端和边缘设备上尤其重要因为那些地方内存本来就紧张。5. 把三笔账合起来一个完整的估算实例5.1 选一个具体网络来算光讲公式太干咱们拿一个具体的网络从头到尾算一遍。就用一个简化版的类 VGG 网络输入 224×224×3FP32batch1。网络结构简化如下层操作输出尺寸1Conv 3×3, 64224×224×642Conv 3×3, 64224×224×643MaxPool 2×2112×112×644Conv 3×3, 128112×112×1285Conv 3×3, 128112×112×1286MaxPool 2×256×56×1287Conv 3×3, 25656×56×2568Conv 3×3, 25656×56×2569MaxPool 2×228×28×25610FC 51251211FC 10105.2 权重账逐层算参数量层13×3×3×64 64 1,792层23×3×64×64 64 36,928层43×3×64×128 128 73,856层53×3×128×128 128 147,584层73×3×128×256 256 295,168层83×3×256×256 256 590,080层1028×28×256×512 512 102,761,472全连接是大头层11512×10 10 5,130加起来大约 1.04 亿参数FP32 下约 396 MB。注意这里全连接层一个人就占了 98 MB 左右这也是为什么后来的网络都用全局平均池化替代大全连接层。5.3 特征图账按峰值时刻算假设框架不做复用最坏情况把所有中间激活都留着层1输出224×224×64×4 ≈ 12.8 MB层2输出12.8 MB层4输出112×112×128×4 ≈ 6.4 MB层5输出6.4 MB层7输出56×56×256×4 ≈ 3.2 MB层8输出3.2 MB累加约 45 MB。如果框架做了内存复用实际峰值可能只有 15 MB 到 20 MB 左右因为很多层生命周期不重叠。5.4 临时缓冲账假设框架用 im2col 处理层1输入 224×224×3K3im2col 矩阵 (3×3×3) × (224×224) 27 × 50176 ≈ 1.35M 元素FP32 下约 5.4 MB。层4的 im2col输入 112×112×64K3(3×3×64) × (112×112) 576 × 12544 ≈ 7.2M 元素 ≈ 28.9 MB层7输入 56×56×128K3(3×3×128) × (56×56) 1152 × 3136 ≈ 3.6M 元素 ≈ 14.5 MB临时缓冲峰值大概在 30 MB 左右。5.5 汇总账目内存量级权重~396 MB特征图无复用~45 MB特征图有复用~15-20 MB临时缓冲~30 MB合计有复用~440-450 MB看到没权重 396 MB 反而是大头因为这里有个巨大的全连接层。但如果把全连接换成全局平均池化权重降到 300 MB 以下特征图和临时缓冲的占比就上来了。这就是为什么不同网络结构三笔账的比例会差很多。6. 参数量、MACs、内存三个经常被混为一谈的指标6.1 它们各自衡量什么这三个概念经常被放在一起说但衡量的东西完全不同参数量Parameters模型有多少个可学习的数决定权重内存大小也影响模型容量。MACsMultiply-Accumulate Operations乘加运算次数衡量计算量决定推理耗时。内存Memory运行时占用的存储空间由权重、特征图、临时缓冲共同决定。一个常见的误解是参数量小 内存小。MobileNet 参数量只有 400 万比 VGG 的 1.38 亿小得多但 MobileNet 的 MACs 并不低而且它的特征图内存因为深度可分离卷积的中间层设计在某些配置下反而不小。6.2 为什么深度可分离卷积能省参数但省不了太多内存深度可分离卷积把标准卷积拆成 depthwise 和 pointwise 两步。参数量能降到原来的 1/8 到 1/9这是它最大的优势。但内存方面depthwise 卷积的输出特征图和标准卷积一样大pointwise 卷积1×1的输入输出通道数也没变。所以特征图内存并没有因为深度可分离而显著减少。省的主要是权重内存。我做过一个对比同样输出尺寸的网络标准卷积版本权重 50 MB深度可分离版本权重 7 MB但两者峰值特征图内存都在 30 MB 左右差别不大。所以如果你的瓶颈是特征图内存换深度可分离卷积帮助有限。6.3 一个实用的估算口诀根据我的经验可以记一个粗略的口诀权重内存 ≈ 参数量 × 4 字节FP32特征图峰值内存 ≈ 最大那几层特征图之和 × 2考虑复用不彻底临时缓冲 ≈ 最大 im2col 矩阵大小通常是输入特征图的 3 到 9 倍用这个口诀你拿到任何一个网络看一眼结构就能估出内存量级误差通常在 30% 以内。这个精度对于判断能不能部署到某个设备已经够用了。7. 实测中怎么定位内存到底花在哪了7.1 用框架自带的内存分析工具不同框架有不同的工具。PyTorch 可以用torch.cuda.memory_summary()看显存分配或者用torch.profiler做逐算子分析。TensorFlow 有 memory profiler。ONNX Runtime 可以开 verbose 日志看每个节点的内存。关键是要区分已分配内存allocated和保留内存reserved。框架的内存池会预留比实际需要更多的内存reserved 通常比 allocated 大不少。你看到的占用往往是 reserved实际用的没那么多。7.2 逐层打印内存占用的土办法如果框架工具不好用可以自己插桩。在每层前后记录内存快照打印差值。伪代码大概是这样import torch def hook_fn(module, input, output): if torch.cuda.is_available(): mem torch.cuda.memory_allocated() / 1024 / 1024 print(f{module.__class__.__name__}: {mem:.2f} MB) for name, layer in model.named_children(): layer.register_forward_hook(hook_fn)跑一遍前向就能看到内存是在哪一层涨上去的。这个方法很土但非常有效我早期排查内存问题基本都靠它。7.3 几个常见的误判排查内存问题时有几个坑特别容易踩把 reserved 当成 allocated看到占用 2 GB 就慌了其实实际用的可能只有 800 MB剩下的是内存池预留。忽略框架自身的开销CUDA context、cuDNN 句柄、框架运行时本身就要占几百 MB这部分和模型无关。batch size 没对齐测试用 1生产用 32估算时忘了乘。输入尺寸变化训练用 224推理用 512特征图内存按面积平方增长直接翻 5 倍。注意定位内存问题时一定要在真实的生产配置下测包括 batch size、输入尺寸、精度模式。用测试配置测出来的数字没有参考价值。8. 想省内存从这三笔账分别下手8.1 省权重内存量化是首选权重内存最容易省因为它是静态的。FP32 转 FP16 直接减半转 INT8 减到 1/4。而且量化对精度的影响通常可控尤其是 INT8配合校准calibration在很多视觉任务上精度损失不到 1%。我实测过一个分类网络FP32 权重 90 MBINT8 量化后 23 MBTop-1 精度只掉了 0.4%。这个收益在端侧部署时非常划算。8.2 省特征图内存控制 batch 和输入尺寸特征图内存和 batch size、输入分辨率都是线性或平方关系。降 batch 是最直接的但会影响吞吐。降输入分辨率更狠面积直接平方下降但会影响精度。另一个办法是梯度检查点gradient checkpointing不过那是训练阶段的技术推理用不上。推理阶段能做的就是让框架充分做内存复用选支持内存池的推理引擎。8.3 省临时缓冲选对算子和精度临时缓冲主要靠框架优化。选支持隐式 GEMM 的框架避免朴素 im2col。Winograd 要权衡速度快但内存可能涨。算子融合一定要开ConvBNReLU 融合是标配。精度方面FP16 不仅省权重临时缓冲也减半。在支持 FP16 的硬件上开 FP16 推理通常能省 30% 到 40% 的总内存。8.4 一个综合优化案例我之前优化过一个部署在边缘设备上的检测网络原始配置 FP32、batch8、输入 640×640峰值内存 1.8 GB设备只有 2 GB 可用跑起来很勉强。优化步骤权重 INT8 量化权重从 120 MB 降到 30 MB开启 FP16 推理特征图和临时缓冲减半batch 降到 4特征图内存再减半开启算子融合省下约 20% 中间内存最终峰值内存降到 620 MB 左右设备跑得很轻松精度掉了不到 1 个点。这个案例说明三笔账要一起算单省一笔效果有限。9. 几个我踩过的坑和对应的经验9.1 别信模型小就能跑在低端设备上这是我最早踩的坑。看到一个模型文件才 10 MB就觉得随便什么设备都能跑。结果部署上去发现内存不够。后来才明白模型文件大小只反映权重运行时内存的大头是特征图和临时缓冲跟文件大小关系不大。现在的习惯是拿到模型先算三笔账估出峰值内存再决定部署到哪。这个习惯帮我避免了好几次返工。9.2 内存池的 reserved 会骗人有一次监控显示进程占用 3 GB我以为出问题了查了半天。后来发现是框架的内存池预留了大量空间实际 allocated 只有 900 MB。内存池的设计是宁可多留不愿频繁申请所以 reserved 偏高是正常的。判断是否真的内存不足要看 allocated 和系统可用内存而不是看 reserved。9.3 输入尺寸的平方效应最容易被低估把输入从 224 改成 448很多人以为内存翻倍其实是翻 4 倍面积平方。如果网络里有全连接层输入尺寸变化还会导致全连接层参数量剧变权重内存也跟着涨。这个坑我在做超分辨率相关项目时踩过输入尺寸一调内存直接爆了。9.4 残差连接对内存复用的影响前面提过残差连接会拉长张量生命周期降低内存复用效率。如果你的网络残差特别多比如 DenseNet 那种密集连接峰值内存会比同等深度的普通网络高不少。设计网络时如果内存紧张要控制残差分支的数量和跨度。10. 写在最后的一点个人体会把卷积的三笔账算清楚之后我看待模型内存问题的心态完全变了。以前是怎么又爆内存了现在是先估一下峰值看看是哪笔账超了。这个转变让我在选型、部署、优化时都有了明确的抓手而不是盲目试错。参数量、MACs、内存这三个指标各有各的用途千万别混着用。参数量看模型容量和权重内存MACs 看计算量和耗时内存看能不能跑起来。三个都要看但看的时候要清楚各自衡量的是什么。最后分享一个我常用的快速估算流程拿到模型先看参数量估权重内存再看最大几层特征图估激活内存最后看有没有大卷积估临时缓冲三个加起来乘个 1.3 的安全系数基本就是峰值内存的量级。这个方法不精确但足够快能在几分钟内判断一个模型能不能部署到目标设备上。

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

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

免费获取报价 →
↑