Déploiement du modèle Pytorch -------- Introduction à TensorRT

TensorRT


Lien de réimpression

introduction

TensorRT (TRT) est un outil qui peut considérablement accélérer l'inférence de modèles d'apprentissage en profondeur. S'il peut être utilisé correctement, il peut considérablement améliorer l'efficacité d'utilisation de notre GPU et la vitesse de fonctionnement de nos modèles.

TensorRT (TRT) est un cadre d'inférence GPU rapide, et son processus régulier consiste à utiliser des fichiers de modèle existantsCompiler un moteurAu cours du processus de compilation du moteur, ** trouvera la méthode d'opérateur optimale pour chaque couche d'opération de calcul, de sorte que le moteur compilé puisse être exécuté de manière très efficace. ** Il est très similaire au processus de compilation C ++.

En ce qui concerne les informations pertinentes du TRT, je pense que le responsable NV prévaudra. Pour TRT, son avantage en termes de vitesse dans le raisonnement va de soi, à quel point il est bon, à quelle vitesse il est et s'il convient à votre scénario d'entreprise, cela nécessite que chacun juge par lui-même. Le processus général et certaines optimisations de base sont également expliqués dans les informations officielles de NV. Vous pouvez vous référer à cet article de ticket deploying-deep-learning-nvidia-tensorrt . Pour une compréhension rapide de TRT et du processus d'installation, vous pouvez également vous référer à TensorRT-Introduction-Use-Install

Construction de modèles

TRT compile la structure et les paramètres du modèle ainsi que la méthode de calcul du noyau correspondante dans un moteur binaire, accélérant ainsi considérablement la vitesse d'inférence après le déploiement. Afin de pouvoir utiliser TRT pour le raisonnement, une énergie doit être créée. Il existe deux façons de créer un moteur dans TRT:

  • Compilé par la structure du modèle de réseau et les fichiers de paramètres, il est très lent.
  • La lecture d'un moteur existant (fichier gie) est plus rapide car elle ignore le processus d'analyse du modèle et ainsi de suite.

La première méthode est très lente, mais lorsque vous déployez un modèle pour la première fois, ou que vous modifiez la précision du modèle, le type de données d'entrée, la structure du réseau, etc., tant que le modèle est modifié, il doit être recompilé (en En fait, il existe un autre TRT qui peut être rechargé. La méthode des paramètres n'est pas traitée dans cet article).

Supposons maintenant que nous utilisions TRT pour la première fois, nous ne pouvons donc choisir que la première façon de créer un moteur. Afin de créer un moteur, nous avons besoin de deux fichiers, la structure du modèle et les paramètres du modèle, ainsi qu'une méthode pour analyser ces deux fichiers. Dans TRT, le moteur est compilé par la IBuilderconduite d'un objet, nous avons donc besoin d'un nouvel IBuilderobjet clé :

nvinfer1::IBuilder *builder = createInferBuilder(gLogger);

gLoggerC'est l'interface de journalisation dans TRT ILogger, héritez de cette interface et créez votre propre objet de journalisation à transmettre.

Pour compiler un moteur, buildervous devez d'abord créer un INetworkDefinitionconteneur en tant que modèle:

nvinfer1::INetworkDefinition *network = builder->createNetwork();

Notez qu'à ce networkstade , il est vide , nous devons remplir la structure et les paramètres du modèle, c'est-à-dire que pour résoudre notre propre structure de modèle et les fichiers de paramètres, y insérer des données.

TRT a officiellement donné trois analyseurs de formats de modèles de frameworks traditionnels, à savoir:

  • ONNX :IOnnxParser parser = nvonnxparser::createParser(*network, gLogger);
  • Caffe :ICaffeParser parser = nvcaffeparser1::createCaffeParser();
  • UFF :IUffParser parser = nvuffparser::createUffParser();

Parmi eux, UFF est le format utilisé pour TensorFlow. Les fichiers correspondants peuvent être analysés en appelant ces trois analyseurs. Avec ICaffeParser, par exemple, appelle sa parseméthode à remplir network.

virtual const IBlobNameToTensor* nvcaffeparser1::ICaffeParser::parse(
    const char* deploy, 
    const char * model, 
	nvinfer1::INetworkDefinition &network, 
	nvinfer1::DataType weightType)

//Parameters
//deploy	    The plain text, prototxt file used to define the network configuration.
//model	        The binaryproto Caffe model that contains the weights associated with the network.
//network	    Network in which the CaffeParser will fill the layers.
//weightType    The type to which the weights will transformed.

De cette façon, vous pouvez obtenir un rempli network, vous pouvez compiler le moteur, il semble que tout est merveilleux ...

Cependant, le TRT actuel n'est pas parfait. Par exemple, de nombreuses opérations de TensorFlow ne sont pas prises en charge, de sorte que les fichiers que vous transmettez ne sont souvent pas analysés du tout (l'un des dilemmes les plus courants des frameworks d'apprentissage en profondeur). Nous devons donc faire ce que vous remplissez network, ce qui doit être appelé interface de bas niveau TRT pour créer la structure du modèle, similaire à vous ou à TensorFlow Caffe.

TRT fournit une interface plus riche afin que vous puissiez créer votre propre réseau directement via ces interfaces, comme l'ajout d'une couche convolutive:

virtual IConvolutionLayer* nvinfer1::INetworkDefinition::addConvolution(ITensor &input, 
                                                                        int nbOutputMaps,
                                                                        DimsHW kernelSize,
                                                                        Weights kernelWeights,
                                                                        Weights biasWeights)		

// Parameters
// input	The input tensor to the convolution.
// nbOutputMaps	The number of output feature maps for the convolution.
// kernelSize	The HW-dimensions of the convolution kernel.
// kernelWeights	The kernel weights for the convolution.
// biasWeights	The optional bias weights for the convolution.

Les paramètres ici ont fondamentalement des significations similaires à celles d'autres frameworks d'apprentissage en profondeur, et il n'y a rien à dire. Encapsulez simplement les données dans la structure de données du TRT. Peut-être que la différence entre la construction du réseau d'entraînement en temps de paix est la nécessité de renseigner les paramètres du modèle, car TRT est un cadre d'inférence et les paramètres sont connus et déterminés. Ce processus consiste généralement à lire le modèle entraîné, à construire le type de structure de données de TRT et à le placer dedans, ce qui signifie que vous devez analyser le fichier de paramètres de modèle par vous-même.

La raison pour laquelle les interfaces de configuration réseau TRT sont plus abondantes , car même avec ces interfaces de bas niveau, beaucoup ne peuvent toujours pas terminer l'opération, ce n'est pas une add*méthode correspondante , sans parler de la réalité des affaires peut également impliquer beaucoup de fonctionnalités personnalisées couches, Par conséquent, il existe une interface de plugin TRT qui vous permet de définir une add*opération. Son flux est hérité des nvinfer1::IPluginV2interfaces, utilisez cuda pour écrire une couche de fonction auto-définie, puis héritez de nvinfer1::IPluginCreatorla préparation de leurs classes de création qui doivent remplacer ses méthodes virtuelles createPlugin. La dernière REGISTER_TENSORRT_PLUGINmacro d' appel pour enregistrer le plugin peut être utilisée. Introduction aux fonctions membres de l'interface du plugin.

// 获得该自定义层的输出个数,比如 leaky relu 层的输出个数为1
virtual int getNbOutputs() const = 0;

// 得到输出 Tensor 的维数
virtual Dims getOutputDimensions(int index, const Dims* inputs, int nbInputDims) = 0;

// 配置该层的参数。该函数在 initialize() 函数之前被构造器调用。它为该层提供了一个机会,可以根据其权重、尺寸和最大批量大小来做出算法选择。
virtual void configure(const Dims* inputDims, int nbInputs, const Dims* outputDims, int nbOutputs, int maxBatchSize) = 0;

// 对该层进行初始化,在 engine 创建时被调用。
virtual int initialize() = 0;

// 该函数在 engine 被摧毁时被调用
virtual void terminate() = 0;

// 获得该层所需的临时显存大小。
virtual size_t getWorkspaceSize(int maxBatchSize) const = 0;

// 执行该层
virtual int enqueue(int batchSize, const void*const * inputs, void** outputs, void* workspace, cudaStream_t stream) = 0;

// 获得该层进行 serialization 操作所需要的内存大小
virtual size_t getSerializationSize() = 0;

// 序列化该层,根据序列化大小 getSerializationSize(),将该类的参数和额外内存空间全都写入到系列化buffer中。
virtual void serialize(void* buffer) = 0;

Nous devons réécrire ici l'implémentation de tout ou partie des fonctions en fonction des fonctions de notre propre calque. Il y a beaucoup de détails ici, et il n'y a aucun moyen de les développer un par un. Lorsque vous avez besoin de le personnaliser, vous besoin de regarder l'API officielle.

Une fois le modèle de réseau créé, le moteur peut être compilé et certains paramètres du moteur sont requis. Par exemple, la précision du calcul, la taille de lot prise en charge, etc., parce que ces paramètres sont différents, le moteur compilé est également différent.

TRT prend en charge le calcul FP16, qui est également la précision de calcul officiellement recommandée, et son réglage est plus simple. Appelez-le directement:

builder->setFp16Mode(true);

De plus, lors de la définition de la précision, il existe une interface pour définir la stratégie stricte:

builder->setStrictTypeConstraints(true);

Cette interface est de savoir s'il faut effectuer une conversion de type strictement en fonction de la précision définie. Si la stratégie stricte n'est pas définie, le TRT peut choisir un type de calcul de précision plus élevée (sans affecter les performances) dans certains calculs.

En plus de la précision, la taille du lot et la taille de l'espace de travail doivent être définies pour s'exécuter:

builder->setMaxBatchSize(batch_size);
builder->setMaxWorkspaceSize(workspace_size);

La taille de lot ici est la plus grande taille de lot qui peut être prise en charge au moment de l'exécution, et une taille de lot inférieure à cette valeur peut être sélectionnée au moment de l'exécution, et l'espace de travail est également défini par rapport à cette taille de lot maximale.

Après avoir défini les paramètres ci-dessus, vous pouvez compiler le moteur.

nvinfer1::ICudaEngine *engine = builder->buildCudaEngine(*network);

La compilation prend beaucoup de temps, attendez patiemment.

Sérialisation et désérialisation du moteur

La compilation du moteur prend beaucoup de temps. Lorsque le modèle, la précision du calcul, la taille du lot, etc. restent inchangés, nous pouvons choisir de sauvegarder le moteur localement pour la prochaine exécution, c'est-à-dire la sérialisation du moteur. TRT fournit une méthode de sérialisation pratique:

nvinfer1::IHostMemory *modelStream = engine->serialize();

Grâce à cet appel, on obtient un flux binaire, qui peut être sauvegardé en écrivant ce flux dans un fichier.

Si vous avez besoin de déployer à nouveau, vous pouvez désérialiser directement le fichier enregistré et ignorer l'étape de compilation.

IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

Utilisez le moteur pour faire des prédictions

Une fois que vous avez le moteur, vous pouvez l'utiliser pour l'inférence.

Créez d'abord un contexte d'inférence. Ce contexte est similaire à un espace de noms et est utilisé pour stocker les variables d'une tâche d'inférence.

IExecutionContext *context = engine->createExecutionContext();

Un moteur peut avoir plusieurs contextes , ce qui signifie qu'un moteur peut effectuer plusieurs tâches de prédiction en même temps.

Ensuite, il y a l'indice de liaison d'entrée et de sortie. La raison de cette étape est que dans le processus de construction du moteur, TRT mappe l'entrée et la sortie à la séquence de numéros d'index, de sorte que nous ne pouvons obtenir les informations de la couche d'entrée et de sortie que par le numéro d'index. Bien que TRT fournisse une interface pour obtenir des numéros d'index par nom, le stockage local peut faciliter les opérations ultérieures.

Nous pouvons d'abord obtenir le nombre d'index:

int index_number = engine->getNbBindings();

Nous pouvons juger si le nombre de nombres est le même que la somme des entrées et sorties de notre réseau. Par exemple, si vous avez une entrée et une sortie, alors le nombre de nombres est de 2. Si ce n'est pas le cas, cela signifie qu'il y a un problème avec le moteur; s'il n'y a pas de problème, nous pouvons obtenir le numéro de série correspondant à l'entrée et à la sortie par nom:

int input_index = engine->getBindingIndex(input_layer_name);
int output_index = engine->getBindingIndex(output_layer_name);

Pour un réseau d'entrée et de sortie commun, le numéro d'index d'entrée est 0 et le numéro d'index de sortie est 1, cette étape n'est donc pas nécessaire.

Ensuite, vous devez allouer de l'espace mémoire pour les couches d'entrée et de sortie. Afin d'allouer de l'espace mémoire vidéo, nous devons connaître les informations dimensionnelles d'entrée et de sortie et le type de données stockées. La représentation des informations dimensionnelles et du type de données dans TRT est la suivante:

class Dims
{
public:
    static const int MAX_DIMS = 8; //!< The maximum number of dimensions supported for a tensor.
    int nbDims;                    //!< The number of dimensions.
    int d[MAX_DIMS];               //!< The extent of each dimension.
    DimensionType type[MAX_DIMS];  //!< The type of each dimension.
};

enum class DataType : int
{
    kFLOAT = 0, //!< FP32 format.
    kHALF = 1,  //!< FP16 format.
    kINT8 = 2,  //!< quantized INT8 format.
    kINT32 = 3  //!< INT32 format.
};

Nous obtenons la dimension de données (dims) et le type de données (dtype) de l'entrée et de la sortie via le numéro d'index, puis ouvrons de l'espace mémoire pour chaque couche de sortie pour stocker le résultat de sortie:

for (int i = 0; i < index_number; ++i)
{
	nvinfer1::Dims dims = engine->getBindingDimensions(i);
	nvinfer1::DataType dtype = engine->getBindingDataType(i);
    // 获取数据长度
    auto buff_len = std::accumulate(dims.d, dims.d + dims.nbDims, 1, std::multiplies<int64_t>());
    // ...
    // 获取数据类型大小
    dtype_size = getTypeSize(dtype);	// 自定义函数
}

// 为 output 分配显存空间
for (auto &output_i : outputs)
{
    cudaMalloc(buffer_len_i * dtype_size_i * batch_size);
}

Ce que cet article donne est un pseudo-code, qui ne représente que la logique, de sorte que certaines fonctions personnalisées simples seront impliquées.

À ce stade, nous avons fait des préparatifs, et maintenant nous pouvons mettre des données dans le modèle pour le raisonnement.

Prédiction avant

L'exécution de la prédiction directe de TRT est asynchrone et le contexte soumet des tâches via un appel de mise en file d'attente:

cudaStream_t stream;
cudaStreamCreate(&stream);
context->enqueue(batch_size, buffers, stream, nullptr);
cudaStreamSynchronize(stream);

Enqueue est une fonction de TRT qui exécute réellement des tâches. Nous devons également implémenter cette interface de fonction lors de l'écriture du plugin. parmi eux:

  • batch_size: Moteur transmis pendant le processus de construction max_batch_size.

  • buffers: C'est un tableau de pointeurs, et son indice correspond au numéro d'index de la couche d'entrée et de sortie. Il stocke le pointeur de données d'entrée et l'adresse de stockage des données de sortie (c'est-à-dire l'adresse de la mémoire vidéo ouverte).

  • stream: Stream est le concept d'une série d'opérations séquentielles dans cuda. Pour notre modèle, toutes les opérations du modèle sont exécutées sur l'équipement spécifié dans l'ordre spécifié par la (structure du réseau).

    Le flux CUDA fait référence à un ensemble d'opérations CUDA asynchrones, qui sont exécutées sur le périphérique dans l'ordre dans lequel le code hôte est appelé. Stream maintient la séquence de ces opérations et autorise ces opérations à entrer dans la file d'attente de travail une fois le prétraitement terminé, et peut également effectuer certaines opérations de requête sur ces opérations. Ces opérations incluent le transfert de données de l'hôte à l'appareil, le lancement du noyau et d'autres actions d'initiation de l'hôte exécutées par l'appareil. L'exécution de ces opérations est toujours asynchrone et le runtime cuda déterminera le moment approprié de ces opérations. Nous pouvons utiliser l'API cuda correspondante pour nous assurer que les résultats obtenus sont obtenus une fois toutes les opérations terminées. Les opérations dans le même flux ont un ordre d'exécution strict , mais différents flux n'ont pas une telle restriction.

Il convient de noter que les tampons de données d'entrée et de sortie dans le tableau sont sur le GPU, par cudaMemcpy(besoin d'ouvrir une mémoire pour stocker à l'avance) sur les données d'entrée vers le GPU copie le CPU. De la même manière, les données de sortie doivent également être copiées du GPU vers le CPU.

Les deux premières phrases créent un flux cuda, et la dernière phrase consiste à attendre la fin du flux asynchrone, puis à copier les données de la mémoire vidéo.

À ce stade, nous avons terminé un processus de prévision de base de TRT.

Pour résumer

Cet article décrit uniquement le processus de prédiction TRT et certains appels courants, et n'implique pas de réseaux spécifiques et d'implémentations spécifiques, et il n'y a pas trop de détails de codage. Différentes opérations sur différents réseaux nécessitent l'écriture de certains plugins d'extension, et le codage, y compris le développement et la gestion de la mémoire et de la mémoire vidéo, et la déconstruction et le nettoyage de TRT, etc. sortent du cadre de cet article.

Je suppose que tu aimes

Origine blog.csdn.net/ahelloyou/article/details/114870232
conseillé
Classement