Déploiement de modèles d'IA - Implémentation Python de la quantification INT8 du modèle TensorRT

Déploiement du modèle d'IA : implémentation Python de la quantification INT8 du modèle TensorRT

Cet article a été publié pour la première fois sur le compte public [DeepDriving], bienvenue pour y prêter attention.

Aperçu

32À l'heure actuelle, les paramètres des modèles d'apprentissage profond sont essentiellement exprimés en bits à virgule flottante ( ) pendant la phase de formation FP32, de sorte qu'une plage dynamique plus large puisse être utilisée pour mettre à jour les paramètres pendant le processus de formation. Cependant, dans la phase d'inférence, FP32la précision utilisée consommera plus de ressources informatiques et d'espace mémoire. Pour cette raison, lors du déploiement du modèle, une méthode de réduction de la précision du modèle est souvent utilisée, en utilisant la virgule flottante ( ) ou la 16signature binaire FP16. 8entier ( INT8) Pour représenter. Il n'y a généralement aucune perte de précision lors FP32de la conversion en , mais la conversion en 2 peut entraîner une perte de précision plus importante, en particulier lorsque les poids du modèle sont répartis sur une plage dynamique importante.FP16FP32INT8

Bien qu'il y ait une certaine perte de précision, la conversion INT8apportera également de nombreux avantages, tels que la réduction CPUde l'occupation de l'espace de stockage et de la mémoire, l'amélioration du débit de calcul, etc. Ceci est très significatif dans les plates-formes embarquées aux ressources informatiques limitées.

Pour convertir le tenseur des paramètres du modèle FP32de en INT8, c'est-à-dire la plage à laquelle la plage dynamique du tenseur à virgule flottante est mappée [-128,127], vous pouvez utiliser la formule suivante :

xq = C lip ( R ound ( xf / scale ) ) x_{q}=Clip(Round(x_{f}/scale))Xq=Clip ( R o u n d ( x _ _f/ échelle ) ) _ _

Parmi eux, C lip ClipCl i pRond RondR o u n d représente respectivement les opérations de troncature et d’arrondi. Comme le montre la formule ci-dessus,FP32la conversionINT8est de définir une échelle de facteurd'échelle.sc a l e est utilisé pour le mappage. Ce processus de mappage est appelé quantification. La formule ci-dessus est une formule de quantification symétrique.

La clé de la quantification est de trouver un facteur d'échelle approprié afin que la précision du modèle quantifié soit aussi proche que possible du modèle d'origine. Il existe deux manières de quantifier un modèle :

  • La quantification post-entraînement ( ) consiste à calculer le facteur d'échelle via un processus d'étalonnage ( ) Post-training quantization,PTQune fois que le modèle est entraîné pour réaliser le processus de quantification.Calibration

  • La formation prenant en compte la quantification ( Quantization-aware training,QAT) calcule le facteur d'échelle pendant le processus de formation du modèle, permettant de compenser l'erreur de précision provoquée par les opérations de quantification et de quantification inverse pendant le processus de formation.

Cet article présente uniquement comment appeler l' TensorRTinterface Pythonpour réaliser INT8la quantification. Concernant INT8les connaissances théoriques de la quantification, puisqu'elles impliquent beaucoup de contenu, j'écrirai un article spécial pour les présenter quand j'aurai le temps.

Implémentation spécifique de la quantification TensorRT INT8

Calibrateur dans TensorRT

Au cours du processus de quantification post-entraînement, TensorRTle facteur d'échelle de chaque tenseur du modèle doit être calculé. Ce processus est appelé calibrage. Le processus d'étalonnage nécessite de fournir des données représentatives afin d' TensorRTexécuter le modèle sur cet ensemble de données, puis de collecter des statistiques pour chaque tenseur afin de trouver un facteur d'échelle optimal. Trouver le facteur d'échelle optimal nécessite d'équilibrer les deux sources d'erreur d'erreur de discrétisation (qui augmente à mesure que la plage représentée par chaque valeur quantifiée devient plus grande) et d'erreur de troncature (dont les valeurs sont limitées aux limites de la plage représentable), fournissant TensorRTplusieurs différents calibrateurs :

  • IInt8EntropyCalibrator2 : Le calibrateur d'entropie actuellement recommandé, par défaut, l'étalonnage se produit avant la fusion des couches, recommandé pour une utilisation CNNdans les modèles.

  • IInt8MinMaxCalibrator : Ce calibrateur utilise toute la plage de la distribution d'activation pour déterminer le facteur d'échelle. Par défaut, l'étalonnage se produit avant la fusion des couches, recommandé NLPdans les modèles pour la tâche.

  • IInt8EntropyCalibrator : Ce calibrateur est TensorRTle calibrateur d'entropie d'origine. Par défaut, l'étalonnage se produit après la fusion des couches, et son utilisation est actuellement obsolète.

  • IInt8LegacyCalibrator : ce calibrateur nécessite un paramétrage par l'utilisateur. Par défaut, l'étalonnage se produit après la fusion des couches et n'est pas recommandé.

TensorRTLors de la création INT8d'un moteur de modèle, les étapes suivantes sont effectuées :

  1. Construisez un 32moteur de modèle binaire, puis exécutez ce moteur sur l'ensemble de données d'étalonnage et enregistrez un histogramme de la distribution des valeurs d'activation pour chaque tenseur ;
  2. Construire une table d'étalonnage à partir de l'histogramme et calculer un facteur d'échelle pour chaque tenseur ;
  3. Construisez un moteur basé sur la table d'étalonnage et la définition du modèle INT8.

Le processus d'étalonnage peut être lent, mais la table d'étalonnage générée lors de la deuxième étape peut être sortie dans un fichier et peut être réutilisée. Si le fichier de table d'étalonnage existe déjà, le calibrateur lira la table d'étalonnage directement à partir du fichier sans exécuter la précédente deux étapes. De plus, contrairement aux fichiers moteur, les tables d’étalonnage peuvent être utilisées sur toutes les plateformes. Ainsi, lors du déploiement proprement dit du modèle, nous pouvons d’abord GPUgénérer la table d’étalonnage sur un ordinateur généraliste puis Jetson Nanol’utiliser sur une plateforme embarquée. Pour faciliter le codage, nous pouvons utiliser Pythonla programmation pour mettre en œuvre INT8le processus de quantification afin de générer une table d'étalonnage.

Mise en œuvre
1. Charger les données d'étalonnage

Définissez d'abord une classe de chargement de données pour le chargement des données d'étalonnage. Les données d'étalonnage ici sont une JPGimage au format. Une fois l'image lue, elle doit être mise à l'échelle, normalisée, échangée des canaux et d'autres opérations de prétraitement en fonction des exigences de données d'entrée du modèle:

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

Le code de l'opération de prétraitement est le suivant :

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. Implémenter le calibrateur

Pour implémenter la fonction d'un calibrateur, vous devez hériter de TensorRTl'une des quatre classes de calibrateur fournies, puis remplacer plusieurs méthodes du calibrateur parent :

  • get_batch_size: batchtaille utilisée pour obtenir
  • get_batch: utilisé pour obtenir batchles données d' un
  • read_calibration_cache: Permet de lire la table d'étalonnage à partir d'un fichier
  • write_calibration_cache: Utilisé pour écrire la table d'étalonnage de la mémoire vers un fichier

Puisque c'est le modèle que j'ai besoin de quantifier CNN, je choisis d'hériter IInt8EntropyCalibrator2du calibrateur :

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. Générer le moteur INT8

FP32J'ai déjà présenté le processus de génération de moteurs de modèles dans un article, mais dans cet article, il a été C++implémenté. L'appel Pythonde l'implémentation de l'interface est en réalité plus simple. Le code spécifique est le suivant :

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

Le code ci-dessus est d'abord OnnxParserutilisé pour analyser le modèle, puis configdéfinir la précision du moteur. Si vous construisez INT8un moteur, vous devez définir les paramètres correspondants Flaget y transmettre l'objet calibrateur précédemment implémenté, afin que les TensorRTdonnées d'étalonnage soient automatiquement lues pour générer une table d'étalonnage lors de la construction du moteur.

Résultats de test

Afin de vérifier INT8l'effet de la quantification, j'ai fait un test comparatif sur YOLOv5plusieurs modèles que j'ai utilisés sur la carte graphique. GeForce GTX 1650 TiLes résultats du test d'inférence prenant du temps avec différentes précisions sont les suivants :

Modèle Entrez les dimensions Précision du modèle Temps de raisonnement (ms)
yolov5s.onnx 640x640 INT8 7
yolov5m.onnx 640x640 INT8 dix
yolov5l.onnx 640x640 INT8 15
yolov5s.onnx 640x640 FP32 12
yolov5m.onnx 640x640 FP32 23
yolov5l.onnx 640x640 FP32 45

yolov5lLes résultats de détection de cible du modèle FP32et INT8la précision sont présentés dans les deux images suivantes :

On constate que les résultats des tests sont encore relativement proches.

Les références

  1. https://developer.nvidia.com/zh-cn/blog/tensorrt-int8-cn/
  2. https://developer.nvidia.com/blog/chieving-fp32-accuracy-for-int8-inference-using-quantization-aware-training-with-tensorrt/
  3. https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-861/developer-guide/index.html#working-with-int8
  4. https://github.com/xuanandsix/Tensorrt-int8-quantization-pipline.git

Je suppose que tu aimes

Origine blog.csdn.net/weixin_44613415/article/details/131850160
conseillé
Classement