Implantação do modelo de IA: implementação em Python da quantificação INT8 do modelo TensorRT
Este artigo foi publicado pela primeira vez na conta pública [DeepDriving], fique atento.
Visão geral
32
Atualmente, os parâmetros dos modelos de aprendizado profundo são basicamente expressos em ponto flutuante de bits ( ) durante a fase de treinamento FP32
, para que uma faixa dinâmica maior possa ser usada para atualizar os parâmetros durante o processo de treinamento. Porém, na etapa de inferência, FP32
a precisão utilizada consumirá mais recursos computacionais e espaço de memória. Por esse motivo, ao implantar o modelo, muitas vezes é utilizado um método para reduzir a precisão do modelo, utilizando ponto flutuante de bit ( ) ou sinal 16
de bit FP16
. 8
inteiro ( INT8
) Para representar. Geralmente não há perda de precisão na FP32
conversão para , mas a conversão para 2 pode causar uma perda maior de precisão, especialmente quando os pesos do modelo são distribuídos em uma grande faixa dinâmica.FP16
FP32
INT8
Embora haja uma certa perda de precisão, a conversão INT8
também trará muitos benefícios, como redução CPU
da ocupação de espaço de armazenamento e memória, melhoria do rendimento computacional, etc. Isso é muito significativo em plataformas embarcadas com recursos computacionais limitados.
Para converter o tensor de parâmetro do modelo de FP32
para INT8
, ou seja, o intervalo para o qual o intervalo dinâmico do tensor de ponto flutuante é mapeado [-128,127]
, você pode usar a seguinte fórmula:
xq = C lip (Round (xf / escala)) x_{q}=Clip(Round(x_{f}/scale))xq=Clipe ( R o u n d ( x _ _f/ sc a l e ))
Entre eles, C lip ClipCl i p和R ound RoundRound representa operações de truncamento e arredondamento , respectivamente . Como pode ser visto na fórmula acima,FP32
a conversãoINT8
é definir umaescala de fator de escalaescala é usada para mapeamento. Este processo de mapeamento é chamado de quantização. A fórmula acima é uma fórmula de quantização simétrica .
A chave para a quantização é encontrar um fator de escala apropriado para que a precisão do modelo quantizado seja o mais próximo possível do modelo original. Existem duas maneiras de quantificar um modelo:
-
A quantização pós-treinamento ( ) consiste em calcular o fator de escala por meio de um processo de calibração ( )
Post-training quantization,PTQ
após o modelo ser treinado para atingir o processo de quantização.Calibration
-
O treinamento com reconhecimento de quantização (
Quantization-aware training,QAT
) calcula o fator de escala durante o processo de treinamento do modelo, permitindo que o erro de precisão causado pelas operações de quantização e quantização inversa seja compensado durante o processo de treinamento.
Este artigo apresenta apenas como chamar a TensorRT
interface Python
para obter INT8
quantificação. Quanto INT8
ao conhecimento teórico de quantificação, por envolver muito conteúdo, escreverei um artigo especial para apresentá-lo quando tiver tempo.
Implementação específica da quantificação TensorRT INT8
Calibrador no TensorRT
Durante o processo de quantização pós-treinamento, TensorRT
é necessário calcular o fator de escala de cada tensor do modelo, processo denominado calibração. O processo de calibração requer o fornecimento de dados representativos para TensorRT
executar o modelo neste conjunto de dados e, em seguida, coletar estatísticas para cada tensor para encontrar um fator de escala ideal. Encontrar o fator de escala ideal requer equilibrar as duas fontes de erro: erro de discretização (que se torna maior à medida que o intervalo representado por cada valor quantizado aumenta) e erro de truncamento (cujos valores são restritos aos limites do intervalo representável), fornecendo TensorRT
vários calibradores diferentes:
-
IInt8EntropyCalibrator2 : O calibrador de entropia atualmente recomendado, por padrão a calibração ocorre antes da fusão da camada, recomendado para uso
CNN
em modelos. -
IInt8MinMaxCalibrator : Este calibrador utiliza toda a faixa da distribuição de ativação para determinar o fator de escala. Por padrão a calibração ocorre antes da fusão das camadas, recomendado
NLP
em modelos para a tarefa. -
IInt8EntropyCalibrator : Este calibrador é
TensorRT
o calibrador de entropia original. Por padrão, a calibração ocorre após a fusão da camada e seu uso está atualmente obsoleto. -
IInt8LegacyCalibrator : Este calibrador requer parametrização do usuário, por padrão a calibração ocorre após a fusão da camada e não é recomendado.
TensorRT
Ao construir INT8
um mecanismo modelo, as seguintes etapas são executadas:
- Construa um
32
mecanismo de modelo de bits, execute-o no conjunto de dados de calibração e registre um histograma da distribuição dos valores de ativação para cada tensor; - Construa uma tabela de calibração a partir do histograma e calcule um fator de escala para cada tensor;
- Construa um mecanismo com base na tabela de calibração e na definição do modelo
INT8
.
O processo de calibração pode ser lento, mas a tabela de calibração gerada na segunda etapa pode ser gerada em um arquivo e reutilizada. Se o arquivo da tabela de calibração já existir, o calibrador lerá a tabela de calibração diretamente do arquivo sem executar o procedimento anterior. dois passos. Além disso, diferentemente dos arquivos do mecanismo, as tabelas de calibração podem ser usadas em várias plataformas. Portanto, durante a implantação real do modelo, podemos primeiro GPU
gerar a tabela de calibração em um computador de uso geral e depois Jetson Nano
usá-la em uma plataforma embarcada. Por conveniência de codificação, podemos usar Python
programação para implementar INT8
o processo de quantização para gerar uma tabela de calibração.
Implementação
1. Carregar dados de calibração
Primeiro defina uma classe de carregamento de dados para carregar dados de calibração. Os dados de calibração aqui são uma JPG
imagem no formato. Depois que a imagem é lida, ela precisa ser dimensionada, normalizada, troca de canais e outras operações de pré-processamento de acordo com os requisitos de dados de entrada do modelo:
class CalibDataLoader:
def __init__(self, batch_size, width, height, calib_count, calib_images_dir):
self.index = 0
self.batch_size = batch_size
self.width = width
self.height = height
self.calib_count = calib_count
self.image_list = glob.glob(os.path.join(calib_images_dir, "*.jpg"))
assert (
len(self.image_list) > self.batch_size * self.calib_count
), "{} must contains more than {} images for calibration.".format(
calib_images_dir, self.batch_size * self.calib_count
)
self.calibration_data = np.zeros((self.batch_size, 3, height, width), dtype=np.float32)
def reset(self):
self.index = 0
def next_batch(self):
if self.index < self.calib_count:
for i in range(self.batch_size):
image_path = self.image_list[i + self.index * self.batch_size]
assert os.path.exists(image_path), "image {} not found!".format(image_path)
image = cv2.imread(image_path)
image = Preprocess(image, self.width, self.height)
self.calibration_data[i] = image
self.index += 1
return np.ascontiguousarray(self.calibration_data, dtype=np.float32)
else:
return np.array([])
def __len__(self):
return self.calib_count
O código da operação de pré-processamento é o seguinte:
def Preprocess(input_img, width, height):
img = cv2.cvtColor(input_img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (width, height)).astype(np.float32)
img = img / 255.0
img = np.transpose(img, (2, 0, 1))
return img
2. Implemente o calibrador
Para implementar a função de um calibrador, você precisa herdar TensorRT
uma das quatro classes de calibrador fornecidas e, em seguida, substituir vários métodos do calibrador pai:
get_batch_size
:batch
tamanho usado para obterget_batch
: usado para obterbatch
dados de umread_calibration_cache
: Usado para ler a tabela de calibração de um arquivowrite_calibration_cache
: Usado para gravar a tabela de calibração da memória em um arquivo
Como é o modelo que preciso quantificar CNN
, opto por herdar IInt8EntropyCalibrator2
o calibrador:
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
class Calibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, data_loader, cache_file=""):
trt.IInt8EntropyCalibrator2.__init__(self)
self.data_loader = data_loader
self.d_input = cuda.mem_alloc(self.data_loader.calibration_data.nbytes)
self.cache_file = cache_file
data_loader.reset()
def get_batch_size(self):
return self.data_loader.batch_size
def get_batch(self, names):
batch = self.data_loader.next_batch()
if not batch.size:
return None
# 把校准数据从CPU搬运到GPU中
cuda.memcpy_htod(self.d_input, batch)
return [self.d_input]
def read_calibration_cache(self):
# 如果校准表文件存在则直接从其中读取校准表
if os.path.exists(self.cache_file):
with open(self.cache_file, "rb") as f:
return f.read()
def write_calibration_cache(self, cache):
# 如果进行了校准,则把校准表写入文件中以便下次使用
with open(self.cache_file, "wb") as f:
f.write(cache)
f.flush()
3. Gere mecanismo INT8
FP32
Já apresentei o processo de geração de motores de modelo em um artigo anterior, mas naquele artigo ele foi C++
implementado. Chamar Python
a implementação da interface é realmente mais simples. O código específico é o seguinte:
def build_engine():
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
config = builder.create_builder_config()
parser = trt.OnnxParser(network, TRT_LOGGER)
assert os.path.exists(onnx_file_path), "The onnx file {} is not found".format(onnx_file_path)
with open(onnx_file_path, "rb") as model:
if not parser.parse(model.read()):
print("Failed to parse the ONNX file.")
for error in range(parser.num_errors):
print(parser.get_error(error))
return None
print("Building an engine from file {}, this may take a while...".format(onnx_file_path))
# build tensorrt engine
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 * (1 << 30))
if mode == "INT8":
config.set_flag(trt.BuilderFlag.INT8)
calibrator = Calibrator(data_loader, calibration_table_path)
config.int8_calibrator = calibrator
else mode == "FP16":
config.set_flag(trt.BuilderFlag.FP16)
engine = builder.build_engine(network, config)
if engine is None:
print("Failed to create the engine")
return None
with open(engine_file_path, "wb") as f:
f.write(engine.serialize())
return engine
O código acima é OnnxParser
usado primeiro para analisar o modelo e depois config
definir a precisão do mecanismo. Se você estiver construindo INT8
um mecanismo, será necessário definir as configurações correspondentes Flag
e passar nele o objeto calibrador implementado anteriormente, para que os TensorRT
dados de calibração sejam lidos automaticamente para gerar uma tabela de calibração ao construir o mecanismo.
Resultado dos testes
Para verificar INT8
o efeito da quantificação, fiz um teste comparativo em YOLOv5
vários modelos que utilizei na placa gráfica. GeForce GTX 1650 Ti
Os resultados do teste demorado de inferência com diferentes precisões são os seguintes:
Modelo | Insira as dimensões | Precisão do modelo | Tempo de raciocínio (ms) |
---|---|---|---|
yolov5s.onnx | 640x640 | INT8 | 7 |
yolov5m.onnx | 640x640 | INT8 | 10 |
yolov5l.onnx | 640x640 | INT8 | 15 |
yolov5s.onnx | 640x640 | FP32 | 12 |
yolov5m.onnx | 640x640 | FP32 | 23 |
yolov5l.onnx | 640x640 | FP32 | 45 |
yolov5l
Os resultados de detecção de alvo do modelo FP32
e INT8
precisão são mostrados nas duas imagens a seguir:
Pode-se observar que os resultados dos testes ainda estão relativamente próximos.
Referências
- https://developer.nvidia.com/zh-cn/blog/tensorrt-int8-cn/
- https://developer.nvidia.com/blog/chiev-fp32-accuracy-for-int8-inference-using-quantization-aware-training-with-tensorrt/
- https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-861/developer-guide/index.html#working-with-int8
- https://github.com/xuanandsix/Tensorrt-int8-quantization-pipline.git