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, FP32
la 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 16
signature binaire FP16
. 8
entier ( INT8
) Pour représenter. Il n'y a généralement aucune perte de précision lors FP32
de 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.FP16
FP32
INT8
Bien qu'il y ait une certaine perte de précision, la conversion INT8
apportera également de nombreux avantages, tels que la réduction CPU
de 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 FP32
de 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 p和Rond RondR o u n d représente respectivement les opérations de troncature et d’arrondi. Comme le montre la formule ci-dessus,FP32
la conversionINT8
est 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,PTQ
une 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' TensorRT
interface Python
pour réaliser INT8
la quantification. Concernant INT8
les 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, TensorRT
le 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' TensorRT
exé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 TensorRT
plusieurs 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
CNN
dans 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é
NLP
dans les modèles pour la tâche. -
IInt8EntropyCalibrator : Ce calibrateur est
TensorRT
le 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é.
TensorRT
Lors de la création INT8
d'un moteur de modèle, les étapes suivantes sont effectuées :
- Construisez un
32
moteur 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 ; - Construire une table d'étalonnage à partir de l'histogramme et calculer un facteur d'échelle pour chaque tenseur ;
- 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 GPU
générer la table d’étalonnage sur un ordinateur généraliste puis Jetson Nano
l’utiliser sur une plateforme embarquée. Pour faciliter le codage, nous pouvons utiliser Python
la programmation pour mettre en œuvre INT8
le 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 JPG
image 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 TensorRT
l'une des quatre classes de calibrateur fournies, puis remplacer plusieurs méthodes du calibrateur parent :
get_batch_size
:batch
taille utilisée pour obtenirget_batch
: utilisé pour obtenirbatch
les données d' unread_calibration_cache
: Permet de lire la table d'étalonnage à partir d'un fichierwrite_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 IInt8EntropyCalibrator2
du 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
FP32
J'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 Python
de 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 OnnxParser
utilisé pour analyser le modèle, puis config
définir la précision du moteur. Si vous construisez INT8
un moteur, vous devez définir les paramètres correspondants Flag
et y transmettre l'objet calibrateur précédemment implémenté, afin que les TensorRT
donné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 INT8
l'effet de la quantification, j'ai fait un test comparatif sur YOLOv5
plusieurs modèles que j'ai utilisés sur la carte graphique. GeForce GTX 1650 Ti
Les 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 |
yolov5l
Les résultats de détection de cible du modèle FP32
et INT8
la 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
- https://developer.nvidia.com/zh-cn/blog/tensorrt-int8-cn/
- https://developer.nvidia.com/blog/chieving-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