





















本文讨论了 ncnn 和 tengine 两个框架在量化推理上的逻辑,并比较了它们之间的区别与联系,以及一点自己的思考。
风雨送春归,飞雪迎春到。
已是悬崖百丈冰,犹有花枝俏。 ---- 教员
------ 大家好啊 我是 暮冬 Z 羡慕
这里仅简单介绍一下量化,不会展开,有机会再详细写一下量化算法。
量化的核心思想是将连续的浮点数域映射到一个更小的、离散的整数集合上。这通过将浮点数值舍入或映射到最近的整数值来实现,同时保持整个模型表示的动态范围。例如,一个原本使用 32 位浮点数表示的权重值,在量化后可能会被转换为 8 位的整数,这样每个权重只需占用更少的存储空间,并且计算时可以利用专门针对整数优化的硬件加速器,从而提高推理速度并降低能耗。

tensorflow 官网上的这张图详细的解释了量化的种类和区别:
下面介绍和对比的 NCNN 和 Tengine 仅仅指的是运行再 CPU 上是采用的 Int8 量化策略,都属于训练后整形量化。

腾讯 NCNN
ncnn 是一个为手机端极致优化的高性能神经网络前向计算框架。 ncnn 从设计之初深刻考虑手机端的部署和使用。 无第三方依赖,跨平台,手机端 cpu 的速度快于目前所有已知的开源框架。 基于 ncnn,开发者能够将深度学习算法轻松移植到手机端高效执行, 开发出人工智能 APP,将 AI 带到你的指尖。 ncnn 目前已在腾讯多款应用中使用,如:QQ,Qzone,微信,天天 P 图等。
一、ncnn 仅对两个部分进行了量化
这也就是说,只有卷积、深度可分离卷积、全连接三种算子采用量化的 int8 推理,其余算子仍然采用 float 推理。这样的方式是合理的,因为很多算子因为运算效率和精度的原因,不适合量化推理,所以分成量化的和非量化的算子是同行的做法。地平线公司生产的征程系列芯片对算子是否采用量化推理作出了更精细的分类(主动量化、被动量化、手动量化),感兴趣的可以学习一下地平线的量化逻辑。

上图是一个简单的神经网络模型经 ncnn 量化之后的结果。
这个模型有两个卷积层 + 一个全连接层,外加一些激活、BN、维度变换层。这里可以看到:
Tengine 框架就不专门介绍算子量化因子的数量了,因为都是一样的。

上图是 ncnn 的量化模型在卷积推理时的逻辑,先忽略掉图中的红线流程,只看蓝线部分的一般流程:
一般流程中,卷积、深度可分离卷积和全连接这些算子都要进行 量化 - 计算 - 反量化过程,其他算子仍然采用 float 推理。为了减少量化和反量化的计算,ncnn 对特殊情况做了优化,也就是红线流程:
这种情况下减少了量化 和反量化的操作,一定程度上提高了运行效率。但是在实际测试中似乎效果不好,原因在于,现在的神经网络在卷积层和卷积层之间,基本上会有偏置、BN、激活等操作,很难会有两个卷积层相连的情况。
下面是 ncnn 卷积算子的量化推理的简单抽象:
// f: float I:int8 Scale:量化因子 in: input w:weight out:output bias:偏置 | |
f_in = I_in * Scale_in | |
f_w = I_w * Scale_w | |
f_out = f_in * f_w + f_bias | |
= I_in * I_w * (Scale_in * Scale_w) + f_bias |
第一行对符号进行了注释。
第二行和第三行是量化的基本等式
最后 输出的是 f_out ,反量化因子是 (Scale_in * Scale_w) ,还要加上 float 类型的 偏置 f_bias

Tengine 由 OPEN AI LAB 主导开发,该项目实现了深度学习神经网络模型在嵌入式设备上的快速、高效部署需求。为实现在众多 AIoT 应用中的跨平台部署,本项目使用 C 语言进行核心模块开发,针对嵌入式设备资源有限的特点进行了深度框架裁剪。同时采用了完全分离的前后端设计,有利于 CPU、GPU、NPU 等异构计算单元的快速移植和部署,降低评估、迁移成本。
尽管 Tengine 已经挺久不再维护了,但是作为一个完全使用 C 语言承担推理部分工作的框架,tengine 还是有轻量、框架结构清晰、易于拓展、容易学习、部署简单等优势,还是非常值得学习的。
// f: float int8:int8 int32:int32 Scale:量化因子 in: input w:weight out:output bias:偏置 | |
f_w = int8_w * scale_w f_out = int8_out * scale_out f_in = int8_in * scale_in | |
f_out = f_w * f_in + int32_bias | |
f_bias = int32_bias * (scale_w * scale_in) | |
f_out = f_w * f_in + f_bias | |
= int8_w * scale_w * int8_in * scale_in + f_bias | |
= int8_w * int8_in * (scale_w * scale_in) + int32_bias * (scale_w * scale_in) | |
= (int8_w * int8_in + int32_bias) * (scale_w * scale_in) | |
int8_out * scale_out = (int8_w * int8_in + int32_bias) * (scale_w * scale_in) | |
int8_out = (int8_w * int8_in + int32_bias) * (scale_w * scale_in / scale_out) |
为了能讲清楚 Tengine 的量化推理逻辑,需要先看懂上面的等式推理。
尽管行数较多,但实际上很简单,只有最基本的多项式推导。首先记一下第一行符号代表的意思,挺直观就不过多介绍了。
(scale_w * scale_in) 。量化后的模型,偏置为 int32 类型。注意肯定不能是 int8 类型,因为其量化因子是 (scale_w * scale_in) ,采用 int8 绝对会溢出。f_out = f_w * f_in + f_bias 中右边的 float 类型全部转为 int 类型,这个等式很直观。此时你也应该看到了将 bias 的量化因子直接定为 (scale_w * scale_in) 的好处了。int8_w * int8_in + int32_bias 乘上量化因子 scale_w * scale_in / scale_out从上面的推理来看,把 scale_w * scale_in / scale_out 作为一个整体,每个卷积算子只需要进行一次量化运算就行了,和 ncnn (每个卷积算子都需要 量化 反量化 两次计算) 相比,足足减少了一半的量化运算。
但是上面的推理忽略了一个问题,激活。基本上每个卷积层后面都跟着激活层,激活层能不能在量化形式下(整形状态)进行呢,这又是一个值得讨论的问题。
tengine 没有去讨论这个问题,将上面的推理分成两步去做,先通过 * scale_w * scale_in 反量化为 float 类型,做激活,然后再通过 / scale_out 量化为 int8 类型,具体过程看下图:

再简单介绍一下过程,这个过程在前面的公式推理中已经基本提到了。
* scale_w * scale_in 反量化为 float 类型/ scale_out 量化为 int8 类型

既然量化和反量化过程费时又费力 ,那么有没有一种可能,多个连续的卷积层条件下,只在首尾进行量化和反量化呢?
本博客将在不久后,通过公式、仿真的形式探讨一下其可能性及条件。
本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,并指明哪一篇博客,我看到一定及时回复!
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。