针对作者的讲解,进行浓缩精华,并做相关的笔记。
01前言
初学者往往不理解模型“大”、“小”的真正含义,就简单的选取计算量作为评价指标,疯狂砍计算量(backbone 换 MobileNet/ShuffleNet、Conv 换成 DepthWise Conv、以及一些奇奇怪怪的融合结构等等),把模型计算量砍了将近 10 倍,结果一部署发现速度并没有快多少,反而是把最初的 ResNet 简单砍掉几个 block 效果更好。
本文不仅仅是为了给出网络的设计建议,更是希望能够有效传达性能优化的基础理论知识,以及性能分析的基本思路,帮助各位同学减少网络设计与部署之间的 gap,更高效的完成网络设计与部署工作。
02常用的模型大小评估指标
目前常用于评价模型大小的指标有:计算量、参数量、访存量、内存占用等,这些指标从不同维度评价了模型的大小。本节仅作简单介绍,熟悉的小伙伴可以跳过此节,直接看后面的分析与探讨。
1. 计算量
计算量可以说是评价模型大小最常用的指标了,很多论文在跟 baseline 进行比较时,都会把计算量作为重要的比较依据。
计算量是模型所需的计算次数,反映了模型对硬件计算单元的需求。计算量一般用 OPs (Operations) ,即计算次数来表示。由于最常用的数据格式为 float32,因此也常常被写作 FLOPs (Floating Point Operations),即浮点计算次数。(这里为了跟传统习惯保持一致,下文就统一采用 FLOPs 啦)
模型的整体计算量等于模型中每个算子的计算量之和。而每个算子的计算量计算方法各不一致。例如对于 Eltwise Sum 来讲,两个大小均为 (N, C, H, W) 的 Tensor 相加,计算量就是 N x C x H x W;而对于卷积来说,计算量公式为(乘加各算一次):
PyTorch 有不少工具可以模型计算量,但需要注意的是这些工具有可能会遗漏一些算子的计算量,将其计算量算成 0,从而导致统计的计算量跟实际计算量有轻微的偏差,不过大多数情况下这些偏差影响不大。
2. 参数量
早期的论文也很喜欢用参数量来评价模型大小。
参数量是模型中的参数的总和,跟模型在磁盘中所需的空间大小直接相关。对于 CNN 来说参数主要由 Conv/FC 层的 Weight 构成,当然其他的一些算子也有参数,不过一般忽略不计了。
参数量往往是被算作访存量的一部分,因此参数量不直接影响模型推理性能。但是参数量一方面会影响内存占用,另一方面也会影响程序初始化的时间。
参数量会直接影响软件包的大小。当软件包大小是很重要的指标时,参数量至关重要,例如手机 APP 场景,往往对 APK 包的大小有比较严格的限制;此外有些嵌入式设备的 Flash 空间很小,如果模型磁盘所需空间很大的话,可能会放不下,因此也会对参数量有所要求。
除了在设计模型时减少参数量外,还可以通过压缩模型的方式降低软件包大小。例如 Caffe 和 ONNX 采用的 Protobuf 就会对模型进行高效的编码压缩。不过压缩模型会带来解压缩开销,会一定程度增加程序初始化的时间。
3. 访存量
访存量往往是最容易忽视的评价指标,但其实是现在的计算架构中对性能影响极大的指标。
访存量是指模型计算时所需访问存储单元的字节大小,反映了模型对存储单元带宽的需求。访存量一般用 Bytes(或者 KB/MB/GB)来表示,即模型计算到底需要存/取多少 Bytes 的数据。
和计算量一样,模型整体访存量等于模型各个算子的访存量之和。对于 Eltwise Sum 来讲,两个大小均为 (N, C, H, W) 的 Tensor 相加,访存量是 (2 + 1) x N x C x H x W x sizeof(data_type),其中 2 代表读两个 Tensor,1 代表写一个 Tensor;而对于卷积来说,访存量公式为:
访存量对模型的推理速度至关重要,设计模型时需要予以关注。
4. 内存占用
内存占用是指模型运行时,所占用的内存/显存大小。一般有工程意义的是最大内存占用,当然有的场景下会使用平均内存占用。这里要注意的是,内存占用 ≠ 访存量。
内存占用在论文里不常用,主要原因是其大小除了受模型本身影响外,还受软件实现的影响。例如有的框架为了保证推理速度,会将模型中每一个 Tensor 所需的内存都提前分配好,因此内存占用为网络所有 Tensor 大小的总和;但更多的框架会提供 lite 内存模式,即动态为 Tensor 分配内存,以最大程度节省内存占用(当然可能会牺牲一部分性能)。
和参数量一样,内存占用不会直接影响推理速度,往往算作访存量的一部分。但在同一平台上有多个任务并发的环境下,如推理服务器、车载平台、手机 APP,往往要求内存占用可控。可控一方面是指内存/显存占用量,如果占用太多,其他任务就无法在平台上运行;另一方面是指内存/显存的占用量不会大幅波动,影响其他任务的可用性。
5. 小结
计算量、参数量、访存量、内存占用从不同维度定义了模型的大小,应根据不同的场合选用合适的指标进行评价。
模型推理速度不单单受模型计算量的影响,也与访存量和一些其他因素息息相关。下文将详细讨论影响模型推理速度的因素。
03 计算量越小,模型推理就越快吗
答案是否定的。
这一问题我将从如下 3 个点进行讨论:
- 计算密度与 RoofLine 模型
- 计算密集型算子与访存密集型算子
- 推理时间
省略------------------
方法论建议:
- 了解目标硬件的峰值算力和内存带宽,最好是实测值,用于指导网络设计和算子参数选择。
- 明确测试环境和实际部署环境的差异,最好能够在实际部署环境下测试性能,或者在测试环境下模拟实际部署环境。
- 针对不同的硬件平台,可以设计不同计算密度的网络,以在各个平台上充分发挥硬件计算能力(虽然工作量可能会翻好几倍【捂脸)。
- 除了使用计算量来表示/对比模型大小外,建议引入访存量、特定平台执行时间,来综合反映模型大小。
- 实测是最准确的性能评估方式,如果有条件快速实测的话,建议以实测与理论分析相结合的方式设计并迭代网络。
- 遇到性能问题时,可以逐层 profiling,并与部署/优化同学保持紧密沟通,具体问题具体分析(适当了解一下计算相关理论的话,可以更高效的沟通)。
网络设计建议:
对于低算力平台(CPU、低端 GPU 等),模型很容易受限于硬件计算能力,因此可以采用计算量低的网络来降低推理时间。
对于高算力平台(GPU、DSP 等),一味降低计算量来降低推理时间就并不可取了,往往更需要关注访存量。单纯降低计算量,很容易导致网络落到硬件的访存密集区,导致推理时间与计算量不成线性关系,反而跟访存量呈强相关(而这类硬件往往内存弱于计算)。相对于低计算密度网络而言,高计算密度网络有可能因为硬件效率更高,耗时不变乃至于更短。
面向推理性能设计网络结构时,尽量采用经典结构,大部分框架会对这类结构进行图优化,能够有效减少计算量与访存量。例如 Conv->BN->ReLU 就会融合成一个算子,但 Conv->ReLU->BN 就无法直接融合 BN 层
算子的参数尽量使用常用配置,如 Conv 尽量使用 3x3_s1/s2、1x1_s1/s2 等,软件会对这些特殊参数做特殊优化。
CNN 网络 channel 数尽量选择 4/8/16/32 的幂次,很多框架的很多算子实现在这样的 channel 数下效果更好(具体用多少不同平台不同框架不太一样)。
框架除了计算耗时外,也处理网络拓扑、内存池、线程池等开销,这些开销跟网络层数成正比。因此相比于“大而浅”的网络,“小而深”的网络这部分开销更大。一般情况下这部分开销占比不大。但在网络算子非常碎、层数非常多的时候,这部分开销有可能会影响多线程的扩展性,乃至于成为不可忽视的耗时因素。
一些其他建议:
除了优化网络结构、推理框架性能外,还可以考虑通过一些其他工程技巧来提升系统整体的性能。例如:对推理服务流水化,并行数据读取与计算的过程,掩盖 IO 延时。
本文介绍了评估模型大小的四个常用指标——计算量、参数量、访存量、内存占用,从 RoofLine 模型入手详细讨论了影响模型推理速度的影响因素,并给出了面向推理速度的模型设计方法论与建议。