CMSIS Quantization

Table of Contents

1. CMSIS Quantization

1.1. output_shift 与 bias_shift

1.1.1. dec_bit

dec_bit 指 float 最多可以左移 dec_bit 而不超过量化范围.

例如, 假设量化范围为 int8, 则 [-128, 127), 若 float 为 2.0, 则 dec_bit 为 5,因为 2.0 << 5 = 64, 而 2.0 << 6 = 128, 所以最大移位为 5

以 int8 量化为例, 计算 dec_bit 的公式为

\(dec\_bit(x)=7 - (np.ceil(np.log2(x)))\)

CMSIS 是对称量化, 且 scale 为 \(2^{dec\_bit(abs(x)_{max})}\)

需要注意的是后面提到的 scale, shift 等都是指 dec_bit, 而不是 \(2^{dec\_bit}\)

1.1.2. 计算 dense layer 的量化参数

实际上 dense layer 涉及到四个量化参数:

  • fi: input scale
  • fw: weight scale
  • fo: output scale
  • fb: bias scale

但 cmsis_nn api 要求提供两个参数做为量化参数:

  • output_shift
  • bias_shift

其中:

  • \(output\_shift = fi+fw-fo\)
  • \(bias\_shift = fi+fw-fb\)

之所以需要这两个参数是因为:

假设要计算 \(o=w*x+b\), w,x,b 是原始的浮点数, W,X,B,O 是分别量化的结果. 如何通过 W,X,B 得到 O?

并不能直接通过 \(O=W*X+B\), 因为各部分是分别量化的, scale 不同, 不能直接运算.

  1. B 与 W*X 相加时, 需要 B 左移 \(f_i+f_w-f_b\) 位变成和 W*X 相同的 scale
  2. \(W*X+(B\ll bias\_shift)\) 的结果需要先右移 \(f_i+f_w\) 得到浮点数, 再左移 \(f_o\) 得到 OUTPUT, 合并起来就是右移 output_shift

所以最终的过程是: \((W*X+ (B \ll bias\_shift)) \gg output\_shift\)

例如:

为便于理解, 把 scale 看作是 10 的幂 (而不是 2 的幂)

假设 input 为 0.1, weight 为 0.01, bias = 0.1.

未量化时 wx+b 的值为 0.001+0.1=0.101

设对 x/w/b/o 量化的 dec_bit 分别为 1/2/1/2; W/X/B/O 分别是 w/x/b/o 量化后的结果.

如何使用 W/X/B 得到 O?

\(wx+b=o \implies wx+b=(\frac{X}{10})*(\frac{W}{100})+\frac{B}{10}=\frac{XW}{1000}+\frac{B}{10}=\frac{XW+100B}{1000}=\frac{O}{100}\implies \frac{XW+100B}{10}=O\)

最后的式子显示于 B 需要左移 2 (fi+fw-fb), 且 output 需要右移 1 (fi+fw-fo) 才能最终得于 O

用 tflite 的量化模型进行推理时, 在 Prepare 阶段同样使用 output_shift, 不过名字叫 real_multiplier

Backlinks

Quantization (Quantization > Overview): 其中两个 layer 之间的 `DQ–Q` 是一个常量, 可以提前转换成近似的整数操作 (例如 cmsis 的 output_shift 和 tflite 的 real_multiplier)

1.2. weight 量化

根据 weight 本身的范围确定 weight 的 dec_bit, 然后令 W=w*(2**dec_bit) 即可

dec_bit = 7 - (np.ceil(np.log2(np.max([abs(min_weight), abs(max_weight)], axis=0))))

1.3. activiation 量化

使用一些参考输入, 确定每一层的输入输出的范围, 计算各自的 dec_bit 做为相应的 fi, fo, 配合 fw 可以获得 output_shift 和 bias_shift

1.4.

  1. keras layer activation

    keras.Layers.Dense(16, activiation="softmax"), 这一层的输出会包含 softmax 的结果, 所以使用这一层的输出计算的量化参数并不是 dense layer 的参数.

    解决的方法是把 softmax 做为单独的一层, 不要和 dense 写在一起

  2. ReLU 导致的问题

    假设网络结构为:

    FC1 -> ReLU -> FC2

    正常情况下, cmsis relu 的代码应该是这样的:

    void relu(input, output_shift, ouput) {
        /* cmsis 是对称量化, 所以 input_bias=0 */
        int input_bias = 0;
        if (input > input_bias) {
            return input >> output_shift
        }
        return 0
    }
    

    但实际上 cmsis 的 relu api 并不支持 output_shift 参数, 或者说 output_shift 为 0, 即 relu 的 output_scale 等于 input_scale (因为 output_shift=input_scale-output_scale)

    所以 FC2 的 input_scale 应该直接使用 ReLU 的 input_scale, 而不能自己根据 FC2 input 范围来计算, 相当于量化时 relu 被 `pass throught`.

    除了 ReLU, CMSIS 中的 average pooling, max pooling, 也有类似的问题 (softmax 是否有这个问题还不确定…)

    Q: FC, CONV 为什么不像 ReLU 一样直接忽略 output_shift?

    A: FC, CONV 会进行乘加, 结果会很快超出 int8 范围, output_shift 相当于是通过 Q(DQ(output_1, dec_bit_output_1), dec_bit_input_2) 保证输出在 int8 范围内. 但 ReLU, pooling 不存在这个问题

  3. 训练的越多越不准确…

    最初的模型使用了 dropout 做正则化 (而没有使用 batch norm 或 l1/l2 regularizer), 导致训练的越多 weight 越大 (从 0.x 增加到 2.x), 而 weight 变大又导致 activiation 变大 (从几十增加到几千), 而 cmsis-nn 的 output_shift 只支持右移, 即它假设 activiation 总是在 [-128,128) 范围内, 当 activiation 远远大于这个范围时, 缺失的左移操作会导致结果不准确.

    解决的方法:

    1. 使用 batch norm, 保证 activiation 在一定范围
    2. 使用 l1/l2 regularizer, 保证 weight 在较小的范围

1.5. Sample

1.5.1. keras 模型

def cnn():
    inputs = keras.Input(shape=(FRAMES, DCT_COEFFICIENT_COUNT))
    outputs = tf.expand_dims(inputs, axis=-1)

    outputs = layers.Conv2D(
        filters=28, kernel_size=[10, 4], strides=[1, 1], name="conv1"
    )(outputs)
    # outputs = layers.BatchNormalization()(outputs)
    outputs = layers.ReLU()(outputs)
    outputs = layers.Dropout(0.3)(outputs)

    outputs = layers.Conv2D(
        filters=30, kernel_size=[10, 4], strides=[2, 1], name="conv2"
    )(outputs)
    # outputs = layers.BatchNormalization()(outputs)
    outputs = layers.ReLU()(outputs)
    outputs = layers.Dropout(0.2)(outputs)

    outputs = layers.Flatten()(outputs)

    outputs = layers.Dense(16, name="dense1")(outputs)
    # outputs = layers.BatchNormalization()(outputs)
    outputs = layers.ReLU()(outputs)
    outputs = layers.Dropout(0.1)(outputs)

    outputs = layers.Dense(128, name="dense2")(outputs)
    outputs = layers.ReLU()(outputs)
    outputs = layers.Dense(len(WORDS), name="dense3")(outputs)
    outputs = layers.Softmax()(outputs)

    return keras.Model(inputs, outputs)

1.5.2. cmsis 模型

void CNN::run_nn(q7_t* in_data, q7_t* out_data) {
    arm_convolve_HWC_q7_basic_nonsquare(
        in_data, CONV1_IN_X, CONV1_IN_Y, CONV1_IN_CH, conv1_wt, CONV1_OUT_CH,
        CONV1_KERNEL_X, CONV1_KERNEL_Y, CONV1_PADDING_X, CONV1_PADDING_Y,
        CONV1_STRIDE_X, CONV1_STRIDE_Y, conv1_bias, CONV1_BIAS_SHIFT,
        CONV1_OUT_SHIFT, buffer1, CONV1_OUT_X, CONV1_OUT_Y, (q15_t*)buffer3,
        NULL);
    arm_relu_q7(buffer1, CONV1_OUT_X * CONV1_OUT_Y * CONV1_OUT_CH);
    arm_convolve_HWC_q7_basic_nonsquare(
        buffer1, CONV2_IN_X, CONV2_IN_Y, CONV2_IN_CH, conv2_wt, CONV2_OUT_CH,
        CONV2_KERNEL_X, CONV2_KERNEL_Y, CONV2_PADDING_X, CONV2_PADDING_Y,
        CONV2_STRIDE_X, CONV2_STRIDE_Y, conv2_bias, CONV2_BIAS_SHIFT,
        CONV2_OUT_SHIFT, buffer2, CONV2_OUT_X, CONV2_OUT_Y, (q15_t*)buffer3,
        NULL);
    arm_relu_q7(buffer2, CONV2_OUT_X * CONV2_OUT_Y * CONV2_OUT_CH);
    arm_fully_connected_q7(
        buffer2, dense1_wt, DENSE1_INPUT_DIM, DENSE1_OUTPUT_DIM,
        DENSE1_BIAS_SHIFT, DENSE1_OUT_SHIFT, dense1_bias, buffer1,
        (q15_t*)buffer3);
    arm_relu_q7(buffer1, DENSE1_OUTPUT_DIM);
    arm_fully_connected_q7(
        buffer1, dense2_wt, DENSE2_INPUT_DIM, DENSE2_OUTPUT_DIM,
        DENSE2_BIAS_SHIFT, DENSE2_OUT_SHIFT, dense2_bias, buffer2,
        (q15_t*)buffer3);
    arm_relu_q7(buffer2, DENSE2_OUTPUT_DIM);
    arm_fully_connected_q7(
        buffer2, dense3_wt, DENSE3_INPUT_DIM, DENSE3_OUTPUT_DIM,
        DENSE3_BIAS_SHIFT, DENSE3_OUT_SHIFT, dense3_bias, out_data,
        (q15_t*)buffer3);
}

1.5.3. 量化

import tensorflow as tf
import numpy as np

from tensorflow import keras
from tensorflow.keras import layers, losses, metrics, optimizers, models

tf.keras.backend.clear_session()


def collect_inference_statistics(model, ref_input):
    input = model.layers[0].output
    prev = model.layers[0]
    outputs = []
    for layer in model.layers:
        if layer.name.startswith("conv") or layer.name.startswith("dense"):
            outputs.append(prev.output)
            outputs.append(layer.output)
            prev = layer

    model = keras.Model(input, outputs)

    max_value, min_value = [], []
    for x in ref_input[0:2000]:
        value = [(i.numpy().min(), i.numpy().max()) for i in model(x)]
        min_value.append([v[0] for v in value])
        max_value.append([v[1] for v in value])
        min_value, max_value = (
            np.asarray(min_value).min(axis=0),
            np.asarray(max_value).max(axis=0),
        )
        shift = (
            (7 - (np.ceil(np.log2(np.max([abs(min_value), abs(max_value)], axis=0)))))
            .astype(int)
            .tolist()
        )
    return [(s[0], s[1]) for s in zip(shift[0::2], shift[1::2])]


def quantize(model, shift, ref_input):
    KWS_CMSIS_NN = "model/kws_cmsis_nn.h"

    int_bits = int(np.ceil(np.log2(max(abs(ref_input.min()), abs(ref_input.max())))))
    dec_bits = 7 - int_bits

    with open(KWS_CMSIS_NN, "w") as f:
        f.write("// -*- eval: (sw/large-file-mode); -*-\n")
        f.write("#define MFCC_DEC_BITS {}\n".format(dec_bits))

    i = -1
    for layer in model.layers:
        if layer.name.startswith("conv") or layer.name.startswith("dense"):
            i += 1
            print(layer.name)
            fw = None
            for (t, value) in enumerate(layer.get_weights()):
                int_bits = int(
                    np.ceil(np.log2(max(abs(value.min()), abs(value.max()))))
                )
                dec_bits = 7 - int_bits
                q_value = np.round(value * 2 ** dec_bits)

                if t == 0:
                    shift_name = layer.name + "_out"
                    var_name = layer.name + "_weight"
                    # fi+fw-fo
                    fw = dec_bits
                    shift_bits = shift[i][0] + fw - shift[i][1]
                else:
                    shift_name = layer.name + "_bias"
                    var_name = layer.name + "_bias"
                    shift_bits = shift[i][0] + fw - dec_bits

                if shift_bits < 0:
                    shift_bits = 0

                with open(KWS_CMSIS_NN, "a") as f:
                    f.write(
                        "#define {}_SHIFT {}\n".format(shift_name.upper(), shift_bits)
                    )

                    f.write("#define {} {{".format(var_name.upper()))

                    if len(value.shape) == 4:
                        # tf   : HWNC
                        # cmsis: CHWN
                        q_value_t = np.transpose(q_value, (3, 0, 1, 2))
                    else:
                        q_value_t = np.transpose(q_value)
                        q_value_t.tofile(f, sep=", ", format="%d")
                        f.write("}\n")


if __name__ == "__main__":
    MODEL_PATH = "./model/kws"
    model = keras.models.load_model(MODEL_PATH)

    ref_input = np.load("./temp/test_x.npy")
    shift = collect_inference_statistics(model, ref_input)
    print(shift)
    quantize(model, shift, ref_input)

1.6. cmsis-nn 与 tflite 量化

tflite for mcu 底层支持 cmsis-nn, 但 tflite 的量化规范来自于 gemmlowp, 与前面描述的基于 2^n 的对称量化并不一致. 事实上, cmsis-nn 针对 tflite 的量化需求提供了另外一套 api

tensorflow/tensorflow/lite/micro/tools/make/downloads/cmsis/CMSIS/NN/README.md::Legacy vs TFL micro compliant APIs

Backlinks

Quantization (Quantization > Overview): scale 正常为浮点数, 但有些框架例如 CMSIS 会要求 scale 的值必须是 \(2^{n}\) 的形式, 这样可以避免浮点数乘法, 同时可以用移位代替整数乘法. tflite 会使用浮点数 scale, 但是可以用 rounding_doubling_high_mul 转换为整数乘法.

Quantization (Quantization > CMSIS Quantization): CMSIS Quantization

Author: [email protected]
Date: 2020-09-21 Mon 00:00
Last updated: 2023-12-01 Fri 18:28

知识共享许可协议