NCNN 峰值内存实测:MobileNet 逐层分析

最近在研究推理引擎的内存开销,想搞清楚一个模型在实际推理时到底会占用多少峰值内存,以及各种优化选项(light_mode、fp16、int8)对峰值内存的影响。以 MobileNet 为主要对象,在 x86 平台上用 NCNN 做了一组系统性的测试,顺带也把几个常见网络的数据汇总了一下。

MobileNet 逐层内存推导

先看 MobileNet 的网络结构。总参数量约为 17M bytes,按照”输入 + 输出 + 卷积参数”的方式逐层统计,各层内存占用加起来约为 57M bytes 左右,理论上最大单层内存占用约为 4.8M bytes。

卷积大小输入大小内存占用(bs=1,输入+输出+卷积参数)
conv/s23×3×3×32224×224×32211200 bytes
conv dw/s13×3×32112×112×323212416 bytes
conv/s11×1×32×64112×112×324825088 bytes
conv dw/s23×3×64112×112×644016384 bytes
conv/s11×1×64×12856×56×642441216 bytes
conv dw/s13×3×12856×56×1283215872 bytes
conv/s11×1×128×12856×56×1283276800 bytes
conv dw/s23×3×12856×56×1282011648 bytes
conv/s11×1×128×25628×28×1281335296 bytes
conv dw/s13×3×25628×28×2561614848 bytes
conv/s11×1×256×25628×28×2561867776 bytes
conv dw/s23×3×25628×28×2561012736 bytes
conv/s11×1×256×51214×14×2561126400 bytes
conv dw/s13×3×51214×14×512821248 bytes
conv/s11×1×512×51214×14×5121851392 bytes
… ×5
conv dw/s23×3×51214×14×512520192 bytes
conv/s11×1×512×10247×7×5122398208 bytes
conv dw/s23×3×10247×7×1024438272 bytes
conv/s11×1×1024×10247×7×10244595712 bytes
avg pool7×7×1024
fc1024×10001×1×10244104096 bytes

在 MCUNet 上找到了一组类似的统计数据,但与我的计算结果差别较大,已经发了邮件正在询问中。看了下 MCUNet 的代码,它的统计方式只计算了最大的输入+输出激活值,并没有把权重本身算进去。我也按这种方式重新算了一遍,结果还是不太对。按理来说权重在参与计算时也需要加载进内存,应该一并统计才合理。

实验基准与多网络横向对比

测试在 x86 Linux 平台进行,输入图像大小为 224×224×3,使用 NCNN 框架,模型通过 ONNX 转换而来。这里踩了一些坑:ONNX 转 NCNN 有些操作不支持,在线的 onnx simplifier 不太好用,建议自己 clone 下来本地转。

先做了一个 baseline 实验:只加载相同的运行库、不执行任何推理,峰值内存约为 16M bytes(去掉部分库还可以进一步压缩)。循环推理多次并不会提高峰值内存。加载模型后 NCNN 的 baseline 峰值内存约为 76M bytes。

下表是用 VmPeak 统计的各网络实测数据:

神经网络VGG16AlexNetGoogleNetResNet18ResNet50DenseNet161ShuffleNetV2MobileNetMobileNetV2
模型大小(onnx file)527MB233MB25.2MB44.5MB97.4MB110MB8.67MB16.1MB13.5MB
峰值内存(VmPeak)1601.4MB549.7MB410.8MB473.3MB504.4MB48.2MB74.2MB73.4MB

light_mode 与 fp16/int8 对峰值内存的影响

以 MobileNetV2 为例,逐项开关 NCNN 的量化选项测试峰值内存,结果如下:

light_mode=false,峰值约 86M

light_mode=true,峰值约 76M

light_mode=true,同时开启 fp16(use_fp16_packeduse_fp16_storageuse_fp16_arithmetic 均为 true),峰值仍约 76M

light_mode=true,fp16 全开,再加上 int8(use_int8_storageuse_int8_arithmetic 也为 true),峰值依然没有显著下降:

第一个结论由此得出:单独开 fp16 对降低峰值内存没有效果。必须配合 light_mode=true,才能看到约 10M 的改善——这是通过及时释放中间特征图来实现的内存复用,与量化精度无关。

关于”峰值内存”定义的差异

这里有一个值得注意的问题:论文(尤其是面向嵌入式设备的工作,如 MCUNet)在计算峰值内存时,只统计输入输出特征图的内存占用,不把算子本身的权重算进去——在 MCU 场景下,权重往往存在 Flash 里,推理时直接读取,不占 SRAM,所以这种定义有其合理性。而在 Linux 平台的推理引擎(如 NCNN)上实测时,权重是常驻在内存中的,两种口径下的数字自然不在同一量级。在对比不同来源的数据时需要特别留意这一点。