量化推理是如何把scale转换为定点运算的
本文首发于公众号:
今天应这几位老铁要求写一下量化推理里面浮点运算转定点这个问题:
不过说实话,我翻了 tflite 的源码后还是比较迷糊的,毕竟对 DSP、FPGA 这些芯片一窍不通,因此本文权当是记录探索的过程。
前情回顾
谷歌在论文 Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference 中提出了一种全量化推理的方法 (全量化推理就是所有运算都是定点运算),我在之前的文章中也有所解读。
根据前面这篇文章的介绍,矩阵量化最终可以转换到如下公式:
qi,k3=S1S2S3∑Nj=1(qi,j1−Z1)(qj,k2−Z2)+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_0 和 n,使得:M=2^{-n}M_0 成立,其中 M_0 \in (0.5, 1]。这个 M_0 虽然也是一个小数,但可以用定点数的方式来表达 (这一点后面会再简单介绍一下),2^{-n} 在计算机中可以用比特移位实现,这样一来整个流程就可以用定点运算实现了。
tflite源码实现
tflite 中相关的代码实现,网上的资料几乎没有,后来我只能硬着头皮自己去源码里面摸索,最终确定相关代码应该在这个文件里面:https://github.com/tensorflow/tensorflow/blob/r1.15/tensorflow/lite/kernels/internal/quantization_util.cc#L52
(话说这个文件里面的代码干货满满,注释也比较详细,对做底层框架的同学来说非常值得一读)
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_multiplier 和 shift 分别是转换后的定点小数和位移。
这里面浮点定点转换的核心代码只有这几句:
#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_multiplier 和 shift 之后,Google 会把 quantized_multiplier 左移再从 fp32 转换为 int32 的数值表示 (盲猜跟实际部署到 DSP 这类微处理器有关,因为 DSP 上表示不了浮点数,不过我对这些底层的东西一窍不通,就不再展开介绍了),同时对 shift 溢出做一些异常检查 (不过以我的经验,大部分情况下溢出是不会发生的,而且浮点和定点之间的计算误差也比较小,基本可以接受)。
另外,关于 std::frexp 函数是如何实现的,找到的资料很少,这里就不讲了 (到头来写了个寂寞~囧~)。
总结
这篇文章主要挖了一下 tflite 中如何把 scale 运算转换为定点运算,不过装13失败,没有彻底弄懂,玩硬件的同学可能更熟悉一些。
另外,还是要说明一下,在 CPU 这种现代处理器上,其实浮点运算的能力已经相当强大,再跑量化网络的时候可以用 NCNN 那种方式,直接用浮点运算来做反量化,只有在 DSP 这类微处理器上,由于不支持浮点运算,所以才需要用定点的方式来替代 scale 这个操作,从而完成反量化。


说白了就是如何把一个浮点数用 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)
感谢指教,看头像莫非是高大佬![[吃瓜]](data:image/png;base64,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)
这个函数的实现应该直观一点:https://github.com/google/gemmlowp/blob/master/doc/quantization_example.cc#L210
而 0.9644 x 5 = 4.822,非常接近。这样就实现了定点运算,只要存放,计算遵循一定准则即可
量化明白了,但是QAT量化后INT8参数怎么导出存下来呢?该怎么部署推理呢?推理时还要有伪量化过程么?请问大佬可否讲一下,量化后的部署推理过程。
作者,请问比如我把参数都量化为0-255,那么如果两个大数相乘溢出怎么办?有什么机制可以避免吗?