1. 量化介绍
TensorRT支持使用8位整数来表示量化的浮点值。量化方案是对称均匀量化——量化值用带符号的INT8表示,从量化值到非量化值的转换只是一个简单的乘法。相反转换则使用scale的倒数,并通过rounding和clamping。
量化方案包括激活的量化和权重的量化。
激活的量化方案取决于所选择的校准算法,以找到一个scale s,这个scale s最好地平衡了特定数据的舍入误差和精度误差。TensorRT在下一章节( Post-Training Quantization Using Calibration)详解了支持的不同校准方案。
权重的量化方案通过公式,这里需要找到权值张量的浮点最大值和浮点最小值。
要启用任何量化操作,必须在构建器配置中设置INT8标志。
1.1 量化工作流(workflow)
创建量化网络有两个工作流程:
PTQ:训练后量化(PTQ)是在网络经过训练后得到的尺度因子Scale。TensorRT为PTQ提供了一个称为calibration的工作流。当网络在有代表性的输入数据上执行时,它测量每个激活张量中的激活分布,然后使用该分布来估计张量的scale。
QAT:量化感知训练(QAT)在训练过程中计算尺度因子scale。允许训练过程补偿量化和去量化操作的影响。
TensorRT的Quantization Toolkit(量化工具包)是一个PyTorch库,可以帮助生成可以由TensorRT优化的QAT模型。你也可以使用工具包的PTQ在PyTorch中执行PTQ并导出到ONNX。
1.2 隐式量化和显示量化
量化网络可以用两种方式表示:
在隐式量化网络中,每个量化张量都有一个相关的scale。当读写张量时,scale用于隐式量化和去量化。
当处理隐式量化网络时,TensorRT在应用图优化时将模型视为浮点模型,并适时的使用INT8来优化层执行时间。如果一个层在INT8中运行得更快,那么它就在INT8中执行。否则使用FP32或FP16。在这种模式下,TensorRT只对性能进行优化,你几乎无法控制INT8的使用位置——即使你在API级别显式地设置了一个层的精度,TensorRT也可能在图优化过程中将该层与另一层融合,并丢失它必须在INT8中执行的信息。TensorRT的PTQ能力生成一个隐式量化网络。
在显式量化网络中,在量化值和非量化值之间转换的缩放操作由图中的IQuantizeLayer (C, Python)和IDequantizeLayer (C, Python)节点显式表示——这些节点今后将称为Q/DQ节点。与隐式量化相比,显式形式精确地指定了与INT8之间的转换在哪里执行,优化器只执行由模型语义指定的精确转换,即使添加额外的转换可以提高层的精度(例如,选择FP16内核实现而不是INT8实现)或者添加额外的转换会导致引擎执行更快(例如,选择INT8内核实现来执行指定的具有浮点精度的层,反之亦然)
ONNX使用显式量化表示——当PyTorch或TensorFlow中的模型被导出到ONNX时,框架图中的每个伪量化操作都被导出为Q和DQ。由于TensorRT保留了这些层的语义,所以您可以预期任务的准确性非常接近于框架中看到的准确性。虽然优化保留了量化和去量化的位置,但它们可能会改变模型中浮点操作的顺序,因此结果不会按位相同。
注意,与TensorRT的PTQ相比,在框架中执行QAT或PTQ然后导出到ONNX都将产生显式量化模型。
1.3 Per-Tensor量化和Per- Channel量化
Per-Tensor:用统一的单个缩放值(scale)来缩放整个tensor。
Per- Channel:一个channel一个scale来缩放tensor
2. 设置动态范围
TensorRT提供APIs来直接设置动态范围(该范围必须由量化后的张量表示的范围),以支持隐式量化,其中这些值是在TensorRT之外计算出来的。
API允许使用最小值和最大值来设置一个张量的动态范围。由于TensorRT目前只支持对称范围,所以使用max(abs(min_float), abs(max_float))计算比例。注意,当abs(min_float) != abs(max_float)时,TensorRT使用比配置的更大的动态范围,这可能会增加舍入误差。
在INT8中执行的操作的所有浮点输入和输出都需要动态范围。你可以设置一个张量的动态范围如下:
C:tensor->setDynamicRange(min_float, max_float);
python:tensor.dynamic_range = (min_float, max_float);
3. PTQ校准(calibration)
在PTQ中,TensorRT为网络中的每个张量计算一个scale。这个过程称为校准(calibration),需要您提供有代表性的输入数据,TensorRT在这些数据上运行网络来收集每个激活张量的统计信息。
所需的输入数据量取决于应用程序,但实验表明,大约500张图像足以校准ImageNet分类网络。
给定激活张量的统计量,决定最佳scale并不是一门精确的科学——它需要在量化表示中平衡两个误差来源: 离散化误差(随着每个量化值所表示的范围变大而增加)和截断误差(其中值被固定在可表示范围的极限)。因此,TensorRT提供了多种不同的校准器,以不同的方式计算刻度。
旧的校准器对GPU进行层融合,在执行校准之前优化掉不必要的张量。这在使用DLA时可能会出现问题,因为融合模式可能不同,同时在使用kCALIBRATE_BEFORE_FUSION量化标志可能被覆盖。
校准batch size 也会影响IInt8EntropyCalibrator2和IInt8EntropyCalibrator的截断误差。例如,使用多个小批量校准数据进行校准可能会导致直方图分辨率降低和标度值较差。对于每个校准步骤,TensorRT更新每个激活张量的直方图分布。如果它在激活张量中遇到一个大于当前直方图最大值的值,则直方图范围增加2的幂以容纳新的最大值。这种方法工作良好,除非在最后的校准步骤中发生直方图重新分配,导致最终的直方图中有一半的桶是空的。这样的直方图会产生较差的校准比例尺。这也使得校准容易受到校准批次顺序的影响,即不同的校准批次顺序会导致直方图尺寸在不同的点上增加,从而产生略有不同的校准刻度。为了避免这个问题,校准的批次尽可能大,并确保校准批次是随机的,有相似的分布。
4. 显示量化
当TensorRT检测到网络中存在Q/DQ层时,它使用显式的精确处理逻辑来构建引擎。
Q/DQ网络必须在启用INT8-precision构建器标志的情况下构建:
config->setFlag(BuilderFlag::kINT8);
在显式量化中,INT8和INT8之间表示的网络变化是显式的,因此,INT8不能用作类型约束。
4.1 权重量化
Q/DQ模型的权重必须指定使用FP32数据类型,TensorRT使用IQuantizeLayer的scale对权重进行量化。量化的权重存储在Engine文件中。也可以使用预量化权值,但必须使用FP32数据类型指定。Q节点的刻度值必须设置为1.0F, DQ节点的scale值必须为实际scale值。
4.2 ONNX支持
当在PyTorch或TensorFlow中使用量化感知训练(QAT)训练的模型被导出到ONNX时,框架图中的每个伪量化操作都被导出为一对quantizellinear和dequantizellinear ONNX操作符。
当TensorRT导入ONNX模型时,ONNX QuantizeLinear操作符作为IQuantizeLayer实例导入,ONNX DequantizeLinear操作符作为IDequantizeLayer实例导入。ONNX使用opset 10引入了对QuantizeLinear/DequantizeLinear的支持,并在opset 13中添加了量化轴属性(每个通道量化所需)。PyTorch 1.8引入了使用opset 13将PyTorch模型导出到ONNX的支持。
4.3 TensorRT处理Q/DQ网络
当TensorRT以Q/DQ模式优化网络时,优化过程仅限于不改变网络算术正确性的优化。bit-level精度几乎不可能实现,因为浮点操作的顺序可能产生不同的结果。一般来说,允许这些差异是后端优化的基础,这也适用于将带有Q/DQ层的图转换为使用INT8计算。
Q/DQ层控制网络的计算精度和数据精度。IQuantizeLayer实例通过量化将FP32张量转换为INT8张量,IDequantizeLayer实例通过去量化将INT8张量转换为FP32张量。TensorRT期望在可量化层的每个输入上有一个Q/DQ层对。可量化层指的是可以通过融合IQuantizeLayer和IDequantizeLayer转换为量化层的深度学习层。当TensorRT执行这些融合时,它将量化层替换为使用INT8计算操作实际操作INT8数据的量化层。对于本章使用的图表,绿色表示INT8精度,蓝色表示浮点精度。箭头表示网络激活张量,正方形表示网络层。
下图展示了一个可量化的AveragePool层(蓝色)融合了DQ层和Q层。所有三个层都被量化的AveragePool层(绿色)所取代。
在网络优化过程中,TensorRT会移动Q/DQ层称为Q/DQ传播。传播的目标是最大化可以在低精度下处理的图的比例。因此,TensorRT向后传播Q节点(以便量化尽可能早地发生),向前传播DQ节点(以便去量化尽可能晚地发生)。Q层可以与与量化交换的层交换位置,DQ层可以与与去量化交换的层交换位置。
一个网络层Op(x)可以与量化层交换,如果Q (Op (x)) ==Op (Q (x))
同样,一个网络层Op(x)可以与去量化层交换,如果Op (DQ (x)) ==DQ (Op (x))
下面的图说明了DQ的前向传播和Q的后向传播。这些都是对模型的合法重写,因为Max Pooling可以实现INT8,而且Max Pooling可以与DQ和Q交换。
4.4 Q/DQ层位置建议
网络中Q/DQ层的位置会影响性能和准确性。激进的量化会导致模型精度的下降,因为量化引入了误差。但是量化也可以减少延迟。下面列出了在网络中放置Q/DQ层的一些建议:
1. 量化加权运算的所有输入input(加权运算包括卷积、转置卷积和GEMM):量化权重和激活可以减少带宽需求,同时INT8计算能够加速带宽受限和计算受限的层。
sm7.5和更早的设备可能没有针对所有层的INT8实现。在这种情况下,在构建引擎时,您将遇到could not find any implementation。为了解决这个问题,删除量化失败层的Q/DQ节点。
下图展示了两个TensorRT如何让量化融合卷积层的例子。在左边,只有输入是量化的。在右边,输入和输出都是量化的。
2. 默认情况下,不对加权运算的输出进行量化。有时保留高精度的去量子化输出是有用的。例如,如果线性操作之后是一个激活函数(SiLU,如下图所示),需要更高的精度输入才能产生可接受的精度。
下图展示了线性操作后伴随激活函数的示例:
3. 不要在训练框架中模拟Batch Norm和ReLU融合,因为TensorRT优化会保证保留这些操作的算术语义。下图展示了Batch normalization 与卷积和ReLU融合,同时保持与预融合网络中定义的相同的执行顺序,即在训练网络中不需要模拟bn fold:
4. TensorRT可以在weighted layer(加权层)之后融合element-wise的add操作,这对于带有跳跃式连接的模型(如ResNet和EfficientNet)很有用。element-wise addition layer的第一个输入的精度决定了融合后的输出精度。例如下图中,xf1的精度为浮点数,因此融合卷积的输出被限制为浮点数,尾随的Q层不能与卷积融合。
相比之下,当xf1量子化到INT8时,如下图所示,融合卷积的输出也是INT8,并将尾随的Q层可以与卷积进行融合
5. 为了获得额外的性能,可以尝试量化不使用Q/DQ交换的层。目前,具有INT8输入的非加权层也需要INT8输出,因此要量化输入和输出。
下图展示了一个量化可量化运算的例子。element-wise addition融合了input的DQ层和output的Q层:
6. 如果TensorRT不能将操作与周围的Q/DQ层融合,性能就会下降,所以在添加Q/DQ节点时要保守,并考虑到TensorRT的准确性和性能。
7. 对激活值使用per-tensor量化,对权值使用per-channel量化。实践证明,这种配置可获得最佳的量化精度。通过启用FP16,可以进一步优化引擎延迟。TensorRT尽可能使用FP16而不是FP32(这目前不是所有层类型都支持)。