NCNN 峰值内存实测:MobileNet 逐层分析
最近在研究推理引擎的内存开销,想搞清楚一个模型在实际推理时到底会占用多少峰值内存,以及各种优化选项(light_mode、fp16、int8)对峰值内存的影响。以 MobileNet 为主要对象,在 x86 平台上用 NCNN 做了一组系统性的测试,顺带也把几个常见网络的数据汇总了一下。
MobileNet 逐层内存推导
先看 MobileNet 的网络结构。总参数量约为 17M bytes,按照”输入 + 输出 + 卷积参数”的方式逐层统计,各层内存占用加起来约为 57M bytes 左右,理论上最大单层内存占用约为 4.8M bytes。
| 层 | 卷积大小 | 输入大小 | 内存占用(bs=1,输入+输出+卷积参数) |
|---|---|---|---|
| conv/s2 | 3×3×3×32 | 224×224×3 | 2211200 bytes |
| conv dw/s1 | 3×3×32 | 112×112×32 | 3212416 bytes |
| conv/s1 | 1×1×32×64 | 112×112×32 | 4825088 bytes |
| conv dw/s2 | 3×3×64 | 112×112×64 | 4016384 bytes |
| conv/s1 | 1×1×64×128 | 56×56×64 | 2441216 bytes |
| conv dw/s1 | 3×3×128 | 56×56×128 | 3215872 bytes |
| conv/s1 | 1×1×128×128 | 56×56×128 | 3276800 bytes |
| conv dw/s2 | 3×3×128 | 56×56×128 | 2011648 bytes |
| conv/s1 | 1×1×128×256 | 28×28×128 | 1335296 bytes |
| conv dw/s1 | 3×3×256 | 28×28×256 | 1614848 bytes |
| conv/s1 | 1×1×256×256 | 28×28×256 | 1867776 bytes |
| conv dw/s2 | 3×3×256 | 28×28×256 | 1012736 bytes |
| conv/s1 | 1×1×256×512 | 14×14×256 | 1126400 bytes |
| conv dw/s1 | 3×3×512 | 14×14×512 | 821248 bytes |
| conv/s1 | 1×1×512×512 | 14×14×512 | 1851392 bytes |
| … ×5 | |||
| conv dw/s2 | 3×3×512 | 14×14×512 | 520192 bytes |
| conv/s1 | 1×1×512×1024 | 7×7×512 | 2398208 bytes |
| conv dw/s2 | 3×3×1024 | 7×7×1024 | 438272 bytes |
| conv/s1 | 1×1×1024×1024 | 7×7×1024 | 4595712 bytes |
| avg pool | 7×7×1024 | ||
| fc | 1024×1000 | 1×1×1024 | 4104096 bytes |
在 MCUNet 上找到了一组类似的统计数据,但与我的计算结果差别较大,已经发了邮件正在询问中。看了下 MCUNet 的代码,它的统计方式只计算了最大的输入+输出激活值,并没有把权重本身算进去。我也按这种方式重新算了一遍,结果还是不太对。按理来说权重在参与计算时也需要加载进内存,应该一并统计才合理。
实验基准与多网络横向对比
测试在 x86 Linux 平台进行,输入图像大小为 224×224×3,使用 NCNN 框架,模型通过 ONNX 转换而来。这里踩了一些坑:ONNX 转 NCNN 有些操作不支持,在线的 onnx simplifier 不太好用,建议自己 clone 下来本地转。
先做了一个 baseline 实验:只加载相同的运行库、不执行任何推理,峰值内存约为 16M bytes(去掉部分库还可以进一步压缩)。循环推理多次并不会提高峰值内存。加载模型后 NCNN 的 baseline 峰值内存约为 76M bytes。
下表是用 VmPeak 统计的各网络实测数据:
| 神经网络 | VGG16 | AlexNet | GoogleNet | ResNet18 | ResNet50 | DenseNet161 | ShuffleNetV2 | MobileNet | MobileNetV2 |
|---|---|---|---|---|---|---|---|---|---|
| 模型大小(onnx file) | 527MB | 233MB | 25.2MB | 44.5MB | 97.4MB | 110MB | 8.67MB | 16.1MB | 13.5MB |
| 峰值内存(VmPeak) | 1601.4MB | 549.7MB | — | 410.8MB | 473.3MB | 504.4MB | 48.2MB | 74.2MB | 73.4MB |
light_mode 与 fp16/int8 对峰值内存的影响
以 MobileNetV2 为例,逐项开关 NCNN 的量化选项测试峰值内存,结果如下:
light_mode=false,峰值约 86M:
light_mode=true,峰值约 76M:
light_mode=true,同时开启 fp16(use_fp16_packed、use_fp16_storage、use_fp16_arithmetic 均为 true),峰值仍约 76M:
light_mode=true,fp16 全开,再加上 int8(use_int8_storage、use_int8_arithmetic 也为 true),峰值依然没有显著下降:
第一个结论由此得出:单独开 fp16 对降低峰值内存没有效果。必须配合 light_mode=true,才能看到约 10M 的改善——这是通过及时释放中间特征图来实现的内存复用,与量化精度无关。
关于”峰值内存”定义的差异
这里有一个值得注意的问题:论文(尤其是面向嵌入式设备的工作,如 MCUNet)在计算峰值内存时,只统计输入输出特征图的内存占用,不把算子本身的权重算进去——在 MCU 场景下,权重往往存在 Flash 里,推理时直接读取,不占 SRAM,所以这种定义有其合理性。而在 Linux 平台的推理引擎(如 NCNN)上实测时,权重是常驻在内存中的,两种口径下的数字自然不在同一量级。在对比不同来源的数据时需要特别留意这一点。