我在阅读关于量化的资料(特别是关于int8的),试图弄清楚是否有方法可以避免在将一个节点的输出输入到下一个节点之前对其进行去量化和重新量化。因此,我最终找到了静态和动态量化的定义。根据onnxruntime的说法:
动态量化会在推理时动态计算激活的量化参数(缩放和零点)。[…] 静态量化方法首先使用一组称为校准数据的输入运行模型。在这些运行期间,我们计算每个激活的量化参数。这些量化参数作为常量写入到量化模型中,并用于所有输入。
对我来说,这似乎非常清楚,说明两种方法的区别在于何时计算(去)量化参数(动态量化在推理时进行,静态量化在推理前进行并硬编码到模型中),而不是实际的(去)量化过程本身。
然而,我接触了一些文章/论坛回答,它们似乎指向了不同的方向。这篇文章关于静态量化说:
[…] 重要的是,这个额外的步骤允许我们在操作之间传递量化值,而不是在每次操作之间将这些值转换为浮点数 – 然后再转换回整数 – 这导致了显著的加速。
它似乎在争论静态量化不需要在将一个节点的输出作为输入传递给下一个节点之前对其进行去量化和量化操作。我还发现了一个讨论也持相同观点:
问:[…] 然而,我们的硬件同事告诉我,因为它在通道中有FP缩放和零点,硬件仍然需要支持FP才能实现。他们还争论说,在每个内部阶段,值(输入通道)应该被去量化并转换为FP,然后为下一层再次量化。[…]
答:对于第一个论点你是正确的,因为缩放和零点是FP,硬件需要支持FP进行计算。第二个论点可能不成立,对于静态量化,前一层的输出可以不经过去量化成FP就输入到下一层。也许他们在考虑动态量化,它在两层之间的张量保持为FP。
其他人也给出了相同的回答。
所以我尝试使用onnxruntime.quantization.quantize_static
手动量化一个模型。在继续之前,我必须先说明一下:我不在AI领域,我是为了另一个目的在学习这个话题。所以我谷歌了一下,找到了如何做到这一点的方法,并通过以下代码成功实现了:
import torchimport torchvision as tvimport onnxruntimefrom onnxruntime import quantizationMODEL_PATH = "best480x640.onnx"MODEL_OPTIMIZED_PATH = "best480x640_optimized.onnx"QUANTIZED_MODEL_PATH = "best480x640_quantized.onnx"class QuntizationDataReader(quantization.CalibrationDataReader): def __init__(self, torch_ds, batch_size, input_name): self.torch_dl = torch.utils.data.DataLoader( torch_ds, batch_size=batch_size, shuffle=False) self.input_name = input_name self.datasize = len(self.torch_dl) self.enum_data = iter(self.torch_dl) def to_numpy(self, pt_tensor): return (pt_tensor.detach().cpu().numpy() if pt_tensor.requires_grad else pt_tensor.cpu().numpy()) def get_next(self): batch = next(self.enum_data, None) if batch is not None: return {self.input_name: self.to_numpy(batch[0])} else: return None def rewind(self): self.enum_data = iter(self.torch_dl)preprocess = tv.transforms.Compose([ tv.transforms.Resize((480, 640)), tv.transforms.ToTensor(), tv.transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),])ds = tv.datasets.ImageFolder(root="./calib/", transform=preprocess)# optimisationsquantization.shape_inference.quant_pre_process( MODEL_PATH, MODEL_OPTIMIZED_PATH, skip_symbolic_shape=False)quant_ops = {"ActivationSymmetric": False, "WeightSymmetric": True}ort_sess = onnxruntime.InferenceSession( MODEL_PATH, providers=["CPUExecutionProvider"])qdr = QuntizationDataReader( ds, batch_size=1, input_name=ort_sess.get_inputs()[0].name)quantized_model = quantization.quantize_static( model_input=MODEL_OPTIMIZED_PATH, model_output=QUANTIZED_MODEL_PATH, calibration_data_reader=qdr, extra_options=quant_ops)
然而,结果让我更加困惑。以下图片显示了两个模型图(“原始”模型和量化模型)在netron上的一个片段。这是未量化的模型图。
添加了QuantizeLinear/DequantizeLinear节点的事实可能表明了我正在寻找的答案。然而,这些节点的放置方式对我来说毫无意义:它在量化后立即进行去量化,因此各种Conv、Mul等节点的输入类型仍然是float32张量。我肯定在这里遗漏了(或误解了)一些东西,所以我无法弄清楚我最初寻找的答案:静态量化是否允许将前一个节点的仍然量化的输出直接输入到下一个节点?以及我对上述量化过程的理解有什么错误?
回答:
我是硬件AI领域的人,我强烈推荐阅读我的博客,https://franciscormendes.github.io/2024/05/16/quantization-layer-details/但我在这里会做一个总结。简而言之:如果你愿意,你也可以在层之间传递整数值。考虑矩阵乘法(这不过是神经网络中单层输出,带有权重$W$和偏置$b$),
Y = Wx+b
$Y = Wx + b$
这可以表示为量化乘法(你可以在博客中找到详细信息),
选项1:
$$Y = S_q(X_q-Z_q)S_w(W_q-Z_w) + S_b(b_q-Z_b)$$
然而,你也可以对输出进行量化,
选项2:$$Y_q = \frac{S_xS_w}{S_Y}((X_q-Z_x)(W_q-Z_w)+b) + Z_Y$$
记住$\frac{S_xS_W}{S_Y}$是一个常数,我们在编译时就知道它,所以我们可以认为它是一个定点运算,我们可以将其写成
$$M := \frac{S_xS_W}{S_Y} = 2^{-n}M_0$$ 其中$n$总是在编译时确定的一个固定数字(这对于浮点数来说是不成立的)。因此整个表达式,$$Y_q = M((X_q-Z_x)(W_q-Z_w)+b) + Z_Y$$可以使用整数算术进行,并且层之间交换的所有值都是整数值。所以如果你的硬件只支持INT8,你将使用
使用矩阵乘法示例,完全的INT8量化本质上意味着你可以在不支持任何浮点运算的板上部署神经网络。实际上,当你进行INT8量化时,在层之间传递的是$Y_q$。
选项1(a) :$$Y_q = M_0((X_q-Z_x)(W_q-Z_w)+b) + Z_Y$$。
然而,如果你只需要量化权重和乘法但不量化激活,这意味着你正在利用量化来节省权重的空间并使用整数乘法,但选择在层之间传递浮点值。对于这种情况,PyTorch和Keras也可以输出浮点值,在层之间传递,通过简单地省略量化步骤,因此在这种情况下你不需要量化输出(选项1)
$$Y = S_xS_w(X_q-Z_x)(W_q-Z_w) + S_b(b_q-Z_b)$$