close

12.7 PTQ 量化實踐

前言

上一節介紹了模型量化的概念,本節開始進行PTQ的實踐,並對比不同動態範圍計算方法的差別(max, entropy, mse, pertentile)。

由於量化是對已經訓練好的模型進行量化並評估在測試集上的精度,因此量化(PTQQAT)需要依託一個完成的模型訓練專案開展,這裡採用第八章第一節的分類任務。

PTQ代碼實踐將從以下幾個部分進行:pytorch-quantization庫介紹、推理統計、校準、量化、評估模型準確率和TensorRT推理效率。

pytorch_quantization 工具庫

安裝:

pip install pytorch-quantization --extra-index-url https://pypi.ngc.nvidia.com

 

或者(pytorch 1.12版本時推薦用 pytorch-quantization==2.1.2

 

pip install pytorch-quantization==2.1.2

Copy

pytorch_quantization是由NVIDIA官方提供的PyTorch量化庫,用於將PyTorch模型量化為低比特形式(如Int8)。

pytorch_quantization 庫使用非常方便,其PTQ步驟分四步:

第一步:構建具備量化模組的nn.Module,在模型定義之前替換pytorch官方nn.Module的網路層。如nn.conv2替換為quant_nn.QuantConv2d

Quantxxx這樣的網路層中會在原功能基礎上,添加一些元件,這些元件將在校準時記錄資料、並存儲scaleZ

第二步:正常構建模型,並執行前向推理。

第三步:執行動態範圍計算,並計算scaleZ值。

第四步:匯出ONNX模型

通過以上流程可知道,僅第一步中pytorch_quantization 庫中自訂的量化功能模組需要深入瞭解,其餘都是常規流程。

因此,下面簡單介紹pytorch_quantization中的核心元件。

量化函數——Quantization function

提供兩個量化功能函數 tensor_quant fake_tensor_quant

  • tensor_quant(inputs, amax, num_bits=8, output_dtype=torch.float, unsigned=False)
  •  
    • 功能:對輸入張量進行量化,輸出量化後的張量
  • fake_tensor_quant(inputs, amax, num_bits=8, output_dtype=torch.float, unsigned=False)
  •  
    • 輸入張量進行偽量化,先量化,後反量化,偽量化輸出的張量與原張量存在量化誤差,可用於理解量化誤差的產生。

from pytorch_quantization import tensor_quant

 

# Generate random input. With fixed seed 12345, x should be

# tensor([0.9817, 0.8796, 0.9921, 0.4611, 0.0832, 0.1784, 0.3674, 0.5676, 0.3376, 0.2119])

torch.manual_seed(12345)

x = torch.rand(10)

 

# fake quantize tensor x. fake_quant_x will be

# tensor([0.9843, 0.8828, 0.9921, 0.4609, 0.0859, 0.1797, 0.3672, 0.5703, 0.3359, 0.2109])

fake_quant_x = tensor_quant.fake_tensor_quant(x, x.abs().max())

 

# quantize tensor x. quant_x will be

# tensor([126., 113., 127.,  59.,  11.,  23.,  47.,  73.,  43.,  27.])

# with scale=128.0057

quant_x, scale = tensor_quant.tensor_quant(x, x.abs().max())

Copy

量化描述符和量化器——Descriptor and quantizer

QuantDescriptor 用於描述該採用何種量化方式,例如 QUANT_DESC_8BIT_PER_TENSOR QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL

這裡的pre tensor per channel是一個新知識點,對於activation通常用per tensor 對於卷積核通常用per channel。細節不展開,感興趣可以閱讀《A White Paper on Neural Network Quantization》的2.4.2 Per-tensor and per-channel quantization

描述符是用於初始化量化器quantizer的,量化器可接收張量,輸出量化後的張量。完成量化後,在量化器內部會記錄相應的scaleZ值。

from pytorch_quantization.tensor_quant import QuantDescriptor

from pytorch_quantization.nn.modules.tensor_quantizer import TensorQuantizer

 

quant_desc = QuantDescriptor(num_bits=4, fake_quant=False, axis=(0), unsigned=True)

quantizer = TensorQuantizer(quant_desc)

 

torch.manual_seed(12345)

x = torch.rand(3, 4, 2)

 

quant_x = quantizer(x)

 

print(x)

print(quant_x)

print(quantizer.scale)  # 可以得到3scale,可知道是按照第一個維度切分張量。因為描述符設置了 axis=(0)

Copy

量化模組——Quantized module

前面提到PTQ量化第一步是將pq庫的Quantxxx模組替換掉pytorchnn.Module,例如nn.conv2d變為quant_nn.Conv2d

在實際量化中主要有LinearConv兩大類(nn.Module可以回顧chapter-4),下面對比pytorchpq庫的網路層創建區別。

from torch import nn

 

from pytorch_quantization import tensor_quant

import pytorch_quantization.nn as quant_nn

 

# pytorch's module

fc1 = nn.Linear(in_features, out_features, bias=True)

conv1 = nn.Conv2d(in_channels, out_channels, kernel_size)

 

# quantized version

quant_fc1 = quant_nn.Linear(

    in_features, out_features, bias=True,

    quant_desc_input=tensor_quant.QUANT_DESC_8BIT_PER_TENSOR,

    quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW)

quant_conv1 = quant_nn.Conv2d(

    in_channels, out_channels, kernel_size,

    quant_desc_input=tensor_quant.QUANT_DESC_8BIT_PER_TENSOR,

    quant_desc_weight=tensor_quant.QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL)

Copy

可以發現,只是多了量化描述符的配置,根據量化物件(上一節提到,主要是weight bias activation),通常需要設定不同的量化方式。

例如,對於activationper tensor 對於linear的權重用per row,對於conv的權重用pre channel

ResNet50 PTQ

模型權重傳入百度雲盤,供下載

下面基於chapter-8/01_classification中的項目進行量化,首先訓練一個resnet50,得到的accuracy94.39。模型權重下載-提取碼:jhkm

# 也可以重新訓練 resnet50

nohup python train_main.py --data-path ../data/chest_xray --batch-size 64 --workers 8 --lr 0.01 --lr-step-size 20 --epochs 50 --model resnet50 > ./log.log 2>&1 &

Copy

接下來對它進行量化,並對比accuracy掉點情況以及推理加速情況。

配套代碼位於這裡

流程分析

PTQ整體流程在前言以及介紹,這裡從代碼角度介紹核心流程

# 第一步,初始化pq,將pytorch的模組替換掉

quant_modules.initialize()    # 替換torch.nn的常用層,變為可量化的層

# 第二步,創建模型、載入訓練權重

model = get_model(args, logger, device)

# 第三步,前向推理,統計activation啟動值分佈

collect_stats(model, train_loader, num_batches=args.num_data)  # 設置量化模組開關,並推理,同時統計啟動值

# 第四步,根據動態範圍方法計算scaleZ值,獲得量化後的模型

compute_amax(model, method=args.ptq_method)                     # 計算上限、下限,並計算scale Z

# 第五步,採用量化後的模型進行accuracy評估

loss_m_valid, acc_m_valid, mat_valid = \

    utils.ModelTrainer.evaluate(valid_loader, model, criterion, device, classes)

# 第六步,保存成ONNX模型和pth模型

Copy

第一步中,可以進入pytorch_quantization\quant_modules.py查看當前支持的量化層,量化層中會插入量化器,量化器中存儲量化用的資料——scaleZ

# Global member of the file that contains the mapping of quantized modules

_DEFAULT_QUANT_MAP = [_quant_entry(torch.nn, "Conv1d", quant_nn.QuantConv1d),

                      _quant_entry(torch.nn, "Conv2d", quant_nn.QuantConv2d),

                      _quant_entry(torch.nn, "Conv3d", quant_nn.QuantConv3d),

                      _quant_entry(torch.nn, "ConvTranspose1d", quant_nn.QuantConvTranspose1d),

                      _quant_entry(torch.nn, "ConvTranspose2d", quant_nn.QuantConvTranspose2d),

                      _quant_entry(torch.nn, "ConvTranspose3d", quant_nn.QuantConvTranspose3d),

                      _quant_entry(torch.nn, "Linear", quant_nn.QuantLinear),

                      _quant_entry(torch.nn, "LSTM", quant_nn.QuantLSTM),

                      _quant_entry(torch.nn, "LSTMCell", quant_nn.QuantLSTMCell),

                      _quant_entry(torch.nn, "AvgPool1d", quant_nn.QuantAvgPool1d),

                      _quant_entry(torch.nn, "AvgPool2d", quant_nn.QuantAvgPool2d),

                      _quant_entry(torch.nn, "AvgPool3d", quant_nn.QuantAvgPool3d),

                      _quant_entry(torch.nn, "AdaptiveAvgPool1d", quant_nn.QuantAdaptiveAvgPool1d),

                      _quant_entry(torch.nn, "AdaptiveAvgPool2d", quant_nn.QuantAdaptiveAvgPool2d),

                      _quant_entry(torch.nn, "AdaptiveAvgPool3d", quant_nn.QuantAdaptiveAvgPool3d),]

Copy

第二步,載入模型。

第三步,進行前向推理,但在實際推理時需要對兩個開關分別進行控制。

  • 量化開關:若打開,會進行量化,這個在校準的時候是不需要的。
  • 校準開關:若打開,會在模型前向傳播時,統計資料,這個在校準時是需要的。

因此在推理前有這樣的程式碼片段(同理,推理後需要把兩個開關反過來):

# Enable calibrators

for name, module in model.named_modules():

    if isinstance(module, quant_nn.TensorQuantizer):

        if module._calibrator is not None:

            module.disable_quant()

            module.enable_calib()

        else:

            module.disable()

Copy

第四步中,調用module.load_calib_amax(**kwargs),實現scaleZ值的計算。scaleZ值會存在量化器中。

例如resnet50中的conv1層是一個QuantConv2d,其中包含了input_quantizerweight_quantizer兩個量化器,量化器中存儲了amaxstep_size(scale)

<<AI人工智慧 PyTorch自學>> 12.7 PTQ

第五步中,量化後resnet50模型推理時,量化層會對輸入資料、權重進行量化,然後再進行運算。

這裡debug進入到layer1的第一個卷積層,觀察Quantconv2dforwardpytorch_quantization\nn\modules\quant_conv.py

可以看到,進行運算前,對輸入資料和權重進行量化,量化用到的就是上述提到的量化器,量化器中有scaleZ值。

def forward(self, input):

    quant_input, quant_weight = self._quant(input)

    output = F.conv2d(quant_input, quant_weight, self.bias, self.stride, self.padding, self.dilation, self.groups)

    return output

 

def _quant(self, input):

    quant_input = self._input_quantizer(input)

    quant_weight = self._weight_quantizer(self.weight)

    return (quant_input, quant_weight)

Copy

<<AI人工智慧 PyTorch自學>> 12.7 PTQ

 

具體量化過程,可debug追溯得到如下過程:

quant_weight = self._weight_quantizer(self.weight)

 

 

1 ---> 進入量化器中的forwardpytorch_quantization\nn\modules\tensor_quantizer.py

1 ---> 量化器中可實現校準功能、截斷功能、量化功能,是根據3個屬性開關來判斷,這裡也發現了校準資料收集的代碼。

def forward(self, inputs):

    if self._disabled:

        return inputs

 

    outputs = inputs

 

    if self._if_calib:

        if self._calibrator is None:

            raise RuntimeError("Calibrator was not created.")

        # Shape is only know when it sees the first tensor

        self._calibrator.collect(inputs)

 

    if self._if_clip:

        if not self._learn_amax:

            raise RuntimeError("Clip without learning amax is not implemented.")

        outputs = self.clip(inputs)

 

    if self._if_quant:

        outputs = self._quant_forward(inputs)

 

    return outputs

 

2 ---> 進入量化器中的_quant_forward(),

2 ---> _quant_forward中可以實現偽量化、量化兩種操作,這裡是需要進行量化的,因此會執行

2 ---> outputs, self._scale = tensor_quant(inputs, amax, self._num_bits, self._unsigned)

2 ---> tensor_quant就是開篇介紹pytorch_quantization庫的第一個功能,對張量進行量化的函數。

 

def _quant_forward(self, inputs):

    """Quantized forward pass."""

    if self._learn_amax:

        inputs = self.clip(inputs)

        amax = torch.max(-self.clip.clip_value_min, self.clip.clip_value_max).detach()

    else:

        amax = self._get_amax(inputs)

 

    if self._fake_quant:

        if not TensorQuantizer.use_fb_fake_quant:

            outputs = fake_tensor_quant(inputs, amax, self._num_bits, self._unsigned, self._narrow_range)

        else:

            if inputs.dtype == torch.half or amax.dtype == torch.half:

                raise Exception("Exporting to ONNX in fp16 is not supported. Please export in fp32, i.e. disable AMP.")

            outputs = self._fb_fake_quant(inputs, amax)

    else:

        outputs, self._scale = tensor_quant(inputs, amax, self._num_bits, self._unsigned)

 

    return outputs

Copy

完成量化模型評估後,可進行模型保存為ONNX格式,再用netron查看。

可以看到量化的層會插入QDQ節點,這些節點在TensorRT中將會被使用

<<AI人工智慧 PyTorch自學>> 12.7 PTQ

PTQ 量化實現

執行命令後,可在chapter-8\01_classification\Result\2023-09-26_01-47-40 資料夾下獲得對應的onnx輸出,並可觀察不同方法得到的模型精度

python resnet_ptq.py --mode quantize --num-data 512

Copy

動態範圍計算方法對比

對於resnet_ptq.py,這裡簡單介紹使用方法

  • 根據args.ptq_method的有無決定單個動態範圍方法量化, 還是四種量化方法均量化。
  • 根據args.mode == 'quantize' 還是 evaluate,決定代碼進行量化,還是進行單純的預訓練模型accuracy評估。(用於對比)
  • 其他參數參考get_args_parser()

# 進行四種方法對比。跑512batch

python resnet_ptq.py --mode quantize --num-data 512

 

# 單個方法量化

python resnet_ptq.py --mode quantize --ptq-method max --num-data 512

python resnet_ptq.py --mode quantize --ptq-method entropy --num-data 512

python resnet_ptq.py --mode quantize --ptq-method mse --num-data 512

python resnet_ptq.py --mode quantize --ptq-method percentile --num-data 512

Copy

通過五次實驗,發現PTQ後的Acc存在較大方差,具體資料分佈如下箱線圖所示:

{'entropy': [85.74, 89.26, 91.67, 92.79, 93.27],

'max': [83.17, 90.54, 92.15, 92.79, 94.07],

'mse': [89.42, 91.67, 92.63, 92.79, 93.59],

'percentile': [85.58, 87.34, 91.67, 92.63, 92.79]}

Copy

效果最好的是max,除了一次83%acc之外,其餘效果比較好,並且最高達到94.07%

建議:方法都試試,實在不穩定,考慮QAT

<<AI人工智慧 PyTorch自學>> 12.7 PTQ

 

PTQ 效率對比

為了觀察PTQ帶來的效率提升,下面進行未量化、int8量化,分別在bs=1bs=32時的對比。

首先獲得fp32onnx

 python resnet_ptq.py --mode onnxexport

Copy

此時在Result\2023-09-26_01-47-40下應當擁有所有onnx模型,可以採用trtexec進行效率測試

trtexec --onnx=resnet_50_fp32_bs1.onnx

trtexec --onnx=resnet_50_fp32_bs32.onnx

trtexec --onnx=resnet_50_ptq_bs1_data-num512_percentile_91.03%.onnx --int8

trtexec --onnx=resnet_50_ptq_bs32_data-num512_percentile_91.03%.onnx --int8 --saveEngine=resnet_ptq_int8.engine

Copy

時延(中位數) ms

fp32

int8

 

輸送量 fps

fp32

int8

bs=1

1.84

1.01(↓46%

   

524

860(↑64%

bs=32

26.79

15.4(↓43%

   

37.1*32=1187

64.5*32=2064(↑73.9%

通過實驗,可知道,時延可下降40% 輸送量提高70%左右。

小結

本節首先介紹pytorch-quantization庫中三個核心概念,然後梳理PTQ量化步驟,接著實現了resnet50PTQ量化實驗,最後對量化後的模型在trtexec上進行效率對比。

通過本節實踐可知:

  • int8 可有效提高推理速度和輸送量,具體提高比例需結合具體模型、GPU型號而定。本案例時延降低40%左右,輸送量提高70%左右
  • PTQ量化掉點不穩定,需多次試驗,或進行逐層分析,或逐層設定動態範圍方法
  • 採用pytorch-quantization量化,大體分6步:初始化quanti_modules,載入模型,推理統計,量化獲得sz,精度評估,模型匯出

下一小節,介紹QAT的實現。

 

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 HCHUNGW 的頭像
    HCHUNGW

    HCHUNGW的部落格

    HCHUNGW 發表在 痞客邦 留言(0) 人氣()