量化推理是如何把scale转换为定点运算的

量化推理是如何把scale转换为定点运算的

本文首发于公众号:

今天应这几位老铁要求写一下量化推理里面浮点运算转定点这个问题:

不过说实话,我翻了 tflite 的源码后还是比较迷糊的,毕竟对 DSP、FPGA 这些芯片一窍不通,因此本文权当是记录探索的过程。

前情回顾

谷歌在论文 Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference 中提出了一种全量化推理的方法 (全量化推理就是所有运算都是定点运算),我在之前的文章中也有所解读。

根据前面这篇文章的介绍,矩阵量化最终可以转换到如下公式:

qi,k3=S1S2S3Nj=1(qi,j1Z1)(qj,k2Z2)+Z3q_3^{i,k}=\frac{S_1 S_2}{S_3}\sum_{j=1}^N(q_1^{i,j}-Z_1)(q_2^{j,k}-Z_2)+Z_3 \tag{1}

(具体符号说明请看回之前的文章)

这里面只有 \frac{S_1 S_2}{S_3} 是小数,其他都是整数。

正常来说,如果是跑 CPU 这类处理器,我们是可以先用 int8/int32 把整数相关的运算都计算完,再转到 fp32 上来处理 \frac{S_1 S_2}{S_3} 这个 scale 运算的。但如果是要部署到 DSP 这类微处理器上,就必须做全量化推理,因为这类微处理器是不支持浮点运算的 (你可以简单的理解为上面没有 fp32 这种数据类型)。

假设 M=\frac{S_1 S_2}{S_3},转换为定点运算的方法就是:找到一个 M_0n,使得:M=2^{-n}M_0 成立,其中 M_0 \in (0.5, 1]。这个 M_0 虽然也是一个小数,但可以用定点数的方式来表达 (这一点后面会再简单介绍一下),2^{-n} 在计算机中可以用比特移位实现,这样一来整个流程就可以用定点运算实现了。

tflite源码实现

tflite 中相关的代码实现,网上的资料几乎没有,后来我只能硬着头皮自己去源码里面摸索,最终确定相关代码应该在这个文件里面:github.com/tensorflow/t

(话说这个文件里面的代码干货满满,注释也比较详细,对做底层框架的同学来说非常值得一读)

scale 转定点运算的核心代码如下:

void QuantizeMultiplier(double double_multiplier, int32_t* quantized_multiplier,
                        int* shift) {
  if (double_multiplier == 0.) {
    *quantized_multiplier = 0;
    *shift = 0;
    return;
  }
#ifdef TFLITE_EMULATE_FLOAT
  // If we're trying to avoid the use of floating-point instructions (for
  // example on microcontrollers) then use an alternative implementation
  // that only requires integer and bitwise operations. To enable this, you
  // need to set the define during the build process for your platform.
  int64_t q_fixed = IntegerFrExp(double_multiplier, shift);
#else   // TFLITE_EMULATE_FLOAT
  const double q = std::frexp(double_multiplier, shift);
  auto q_fixed = static_cast<int64_t>(TfLiteRound(q * (1ll << 31)));
#endif  // TFLITE_EMULATE_FLOAT
  TFLITE_CHECK(q_fixed <= (1ll << 31));
  if (q_fixed == (1ll << 31)) {
    q_fixed /= 2;
    ++*shift;
  }
  TFLITE_CHECK_LE(q_fixed, std::numeric_limits<int32_t>::max());
  // A shift amount smaller than -31 would cause all bits to be shifted out
  // and thus all results would be zero. We implement that instead with
  // q_fixed==0, so as to avoid hitting issues with right-shift
  // operations with shift amounts greater than 31. Note that this happens
  // roughly when abs(double_multiplier) < 2^-31 and the present handling means
  // that we're effectively flushing tiny double_multiplier's to zero.
  // We could conceivably handle values in the range (roughly) [32, 63]
  // as 'denormals' i.e. (shift==0, q_fixed < 2^30). In that point of view
  // the present handling is just doing 'flush denormals to zero'. We could
  // reconsider and actually generate nonzero denormals if a need arises.
  if (*shift < -31) {
    *shift = 0;
    q_fixed = 0;
  }
  *quantized_multiplier = static_cast<int32_t>(q_fixed);
}

这个函数接收三个输入:double_multiplier 就是我们要转换的 scale,quantized_multipliershift 分别是转换后的定点小数和位移。

这里面浮点定点转换的核心代码只有这几句:

#ifdef TFLITE_EMULATE_FLOAT
  // If we're trying to avoid the use of floating-point instructions (for
  // example on microcontrollers) then use an alternative implementation
  // that only requires integer and bitwise operations. To enable this, you
  // need to set the define during the build process for your platform.
  int64_t q_fixed = IntegerFrExp(double_multiplier, shift);
#else   // TFLITE_EMULATE_FLOAT
  const double q = std::frexp(double_multiplier, shift);
  auto q_fixed = static_cast<int64_t>(TfLiteRound(q * (1ll << 31)));
#endif  // TFLITE_EMULATE_FLOAT

IntegerFrExp 是 tflite 自己实现的一个功能和 std::frexp 类似的函数,感兴趣的读者可以自己去阅读 (熟悉硬件和定点加速的同学对这段代码一定会有亲切感,但菜鸡如我就读不下去~囧~)

所以核心函数其实是 std::frexp 这个函数。嗯,找了半天,我想看的核心代码居然是一个标准函数库,这就尴尬了。就好像哥白尼翻遍各种书籍想证明日心说,结果有人跟他说这是常识啊!

这个函数根据输入参数不同,有多种重载方式:

它的作用是把一个浮点数分解成一个正规化的小数和一个位移量 (其实就是开头说的 M=2^{-n}M_0)。

看一个例子:

#include <iostream>
#include <cmath>

using namespace std;

int main(int argc, char const *argv[]) {
   float f = 123.45;
   float f1;
   int i;

   f1 = frexp(f, &i);
   cout << f1 << " * " << "2^" << i << " = " << f1 * pow(2, i) << endl;

    f = 0.000123;
    f1 = frexp(f, &i);
    cout << f1 << " * " << "2^" << i << " = " << f1 * pow(2, i) << endl;
   return 0;
}

这个例子里面,我把 123.45 和 0.000123 分别送到 frexp 里面,分解成 M=2^{-n}M_0 的形式,程序输出的结果如下:

0.964453 * 2^7 = 123.45
0.503808 * 2^-12 = 0.000123

对于 123.45 来说,0.964453 就是我们要求的 M_0,而 bitshift 为 +7,表示要左移 7 个 bit。

在求出 quantized_multipliershift 之后,Google 会把 quantized_multiplier 左移再从 fp32 转换为 int32 的数值表示 (盲猜跟实际部署到 DSP 这类微处理器有关,因为 DSP 上表示不了浮点数,不过我对这些底层的东西一窍不通,就不再展开介绍了),同时对 shift 溢出做一些异常检查 (不过以我的经验,大部分情况下溢出是不会发生的,而且浮点和定点之间的计算误差也比较小,基本可以接受)。

另外,关于 std::frexp 函数是如何实现的,找到的资料很少,这里就不讲了 (到头来写了个寂寞~囧~)。

总结

这篇文章主要挖了一下 tflite 中如何把 scale 运算转换为定点运算,不过装13失败,没有彻底弄懂,玩硬件的同学可能更熟悉一些。

另外,还是要说明一下,在 CPU 这种现代处理器上,其实浮点运算的能力已经相当强大,再跑量化网络的时候可以用 NCNN 那种方式,直接用浮点运算来做反量化,只有在 DSP 这类微处理器上,由于不支持浮点运算,所以才需要用定点的方式来替代 scale 这个操作,从而完成反量化。

参考

编辑于 2021-09-25 15:43
发布一条带图评论吧

28 条评论
默认
最新
景行

说白了就是如何把一个浮点数用 n bit 二进制表示出来,最直接的方法就是 for 循环一下。例如,0.3 可以表示成 0.0100110。注意,这里小数点第一位是 0,不是有效位数,浪费了有效 bit,因此,可以进一步表示成 0.1001101*2^(-1),再进一步表示成 1001101 * 2^(-8),由于 2^n 在计算机中可以使用移位来计算,所以,在编译器上 0.3 可以记为(77,-8),解读为一个 8bit 的整数 77 及其移位值 -8。使用 frexp 这是在代码上简化了这个过程。值得注意,frexp 计算出来的底数一定是 [0.5, 1) 之间的,对应的就是二进制表示上小数点后第一位一定是有效数(也即 1)

07-25
Jermmy
作者

感谢指教,看头像莫非是高大佬[吃瓜]

07-27
灵能风暴
又想起来之前看混合精度量化的论文,为了支持混合精度运算基本上每篇都带一个自己的硬件实现,对于我这种硬件一窍不通的小白来说简直噩梦[捂脸][捂脸][捂脸]
2022-02-20
ttf
09-01
王浩璇
大佬有没有用过MQbench做8bit以下的量化啊,我发现使用4bit时会在第二个周期出现梯度爆炸的情况,找了很久找不到原因,请问你有没有遇见过这种情况
2021-11-30
Jermmy
作者
没试过8bit以下的量化哎。。
2021-11-30
Evelyn
所以每个操作都有一个N?不是全模型共享的?
2021-11-02
Jermmy
作者
应该是M吧
2021-11-02
KevenLee
M=2^(-n)M0, M0 是小数啊,怎么定点化啊
2021-10-15
KevenLee
有个不成熟的想法,以 0.964453 * 2^7 = 123.45 为例子,为了方便,我取 小数点后面四位 0.9644,那么0.9644 在浮点表示法中,可以表示为:111 1011,换成整数,就是123,那么,可以用这个123进行运算,比如 x 5, 那么 123 x 5 = 615,换成浮点约为 615/128 = 4.805【这里之所以为定点,就是要记住你的小数位数,比如这里用了 7 位, 那么,615 的二进制为 10 0110 0111, 7 位小数,那么 110 0111 表示小数,剩下的,100 表示整数,合起来就是 4.8046 约 4.805】
而 0.9644 x 5 = 4.822,非常接近。这样就实现了定点运算,只要存放,计算遵循一定准则即可
2021-11-10
Move.L
是不是可以这样理解?因为M0是0.5-1,所以可以最少用30位来表示他的小数部分,最后乘M0的时候,实际上是乘了一个30位的整数?但是这个30位的定点这么大,对于一个32位的系统来说,不就溢出了吗?(是不是32位的系统,M0只能用14位之类的整形来表示?)
2021-11-02
Yobuwen

量化明白了,但是QAT量化后INT8参数怎么导出存下来呢?该怎么部署推理呢?推理时还要有伪量化过程么?请问大佬可否讲一下,量化后的部署推理过程。

2022-09-27
cdbnsucdcdnjs

作者,请问比如我把参数都量化为0-255,那么如果两个大数相乘溢出怎么办?有什么机制可以避免吗?

2022-08-06
Jermmy
所有参数视为一个整体是什么意思?量化都是每一层单独量化的,第一层和最后一层尽管数值相差很大,只要他们不做add或者concat运算,那他们的量化参数是独立的,不影响彼此。
2022-08-07
cdbnsucdcdnjs
没听明白啊。假设我网络训出来第一层参数集中在0-1,最后一层参数很大 好几百。如果我把所有参数视为一个整体,去量化为int8 那么显然性能全丢了。但如果我逐层量化到int8,似乎又有溢出问题
2022-08-07
皮皮卡
2022-05-03
Jermmy
作者
2022-05-03
Jermmy
我抛砖,大家引玉[握手]
2022-05-03
magic
您好,想询问以下,您的M0最后是如何转换成定点数的呢?
2022-04-25
皮皮卡
m0是直接位移
2022-05-03
Jermmy
作者
可以参考这篇zhuanlan.zhihu.com/p/14,写的不算很细,大概是通过一个定点表示的小数和bit位移实现的
2022-04-25
点击查看全部评论
发布一条带图评论吧

文章被以下专栏收录