Comment nous avons augmenté la productivité de Tensorflow au service de 70%

Tensorflow est devenu la plate-forme standard pour l'apprentissage automatique (ML), populaire à la fois dans l'industrie et dans la recherche. De nombreuses bibliothèques, outils et frameworks gratuits ont été créés pour la formation et la maintenance des modèles ML. Le projet Tensorflow Serving aide à maintenir les modèles ML dans un environnement de production distribué.

Notre service Mux utilise Tensorflow Serving dans plusieurs parties de l'infrastructure, nous avons déjà discuté de l'utilisation de Tensorflow Serving dans l'encodage des titres vidéo. Aujourd'hui, nous allons nous concentrer sur les méthodes qui améliorent la latence en optimisant à la fois le serveur de prévisions et le client. Les prévisions de modèle sont généralement des opérations «en ligne» (sur le chemin critique de la demande d'une application), par conséquent, les principaux objectifs de l'optimisation sont de traiter de gros volumes de demandes avec le plus court délai possible.

Qu'est-ce que Tensorflow Serving?


Tensorflow Serving fournit une architecture de serveur flexible pour le déploiement et la maintenance des modèles ML. Une fois que le modèle est formé et prêt à être utilisé pour les prévisions, Tensorflow Serving nécessite de l'exporter dans un format compatible (utilisable).

Servable est une abstraction centrale qui enveloppe les objets Tensorflow. Par exemple, un modèle peut être représenté comme un ou plusieurs objets Servables. Ainsi, Servable sont les objets de base que le client utilise pour effectuer des calculs. La taille utile est importante: les modèles plus petits prennent moins d'espace, utilisent moins de mémoire et se chargent plus rapidement. Pour télécharger et maintenir à l'aide de l'API Predict, les modèles doivent être au format SavedModel.



Tensorflow Serving combine les composants de base pour créer un serveur gRPC / HTTP qui dessert plusieurs modèles ML (ou plusieurs versions), fournit des composants de surveillance et une architecture personnalisée.

Tensorflow au service de Docker


Jetons un coup d'œil aux mesures de base de la latence dans les prévisions de performances avec les paramètres de service Tensorflow standard (sans optimisation du processeur).

Tout d'abord, téléchargez la dernière image à partir du hub TensorFlow Docker:

docker pull tensorflow/serving:latest 

Dans cet article, tous les conteneurs s'exécutent sur un hôte avec quatre cœurs, 15 Go, Ubuntu 16.04.

Exporter le modèle Tensorflow vers SavedModel


Lorsqu'un modèle est formé à l'aide de Tensorflow, la sortie peut être enregistrée en tant que points de contrôle variables (fichiers sur disque). La sortie est effectuée directement en restaurant les points de contrôle du modèle ou dans un format de graphique figé figé (fichier binaire).

Pour Tensorflow Serving, ce graphique figé doit être exporté au format SavedModel. La documentation Tensorflow contient des exemples d'exportation de modèles formés au format SavedModel.

Tensorflow fournit également de nombreux modèles officiels et de recherche comme point de départ pour l'expérimentation, la recherche ou la production.

À titre d'exemple, nous utiliserons le modèle de réseau neuronal résiduel profond (ResNet) pour classer un ensemble de données ImageNet de 1000 classes. Téléchargez le modèle ResNet-50 v2 pré - ResNet-50 v2 , en particulier l' option Channels_last (NHWC) dans SavedModel : en règle générale, cela fonctionne mieux sur le CPU.

Copiez le répertoire du modèle RestNet dans la structure suivante:

 models/ 1/ saved_model.pb variables/ variables.data-00000-of-00001 variables.index 

Tensorflow Serving attend une structure de répertoires ordonnée numériquement pour le contrôle de version. Dans notre cas, le répertoire 1/ correspond au modèle version 1, qui contient l'architecture du modèle saved_model.pb avec un instantané des poids du modèle (variables).

Chargement et traitement de SavedModel


La commande suivante démarre le serveur de modèle Tensorflow Serving dans un conteneur Docker. Pour charger SavedModel, vous devez monter le répertoire model dans le répertoire conteneur attendu.

 docker run -d -p 9000:8500 \ -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet \ -t tensorflow/serving:latest 

La vérification des journaux de conteneur montre que ModelServer est opérationnel pour gérer les demandes de sortie pour le modèle resnet aux points de terminaison gRPC et HTTP:

 ... I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: resnet version: 1} I tensorflow_serving/model_servers/server.cc:286] Running gRPC ModelServer at 0.0.0.0:8500 ... I tensorflow_serving/model_servers/server.cc:302] Exporting HTTP/REST API at:localhost:8501 ... 

Client de prévisions


Tensorflow Serving définit un schéma d'API au format tampons de protocole (protobufs). Les implémentations client GRPC pour l'API de prévision sont empaquetées sous la forme d'un package Python tensorflow_serving.apis . Nous aurons besoin d'un autre tensorflow package Python pour les fonctions utilitaires.

Installez les dépendances pour créer un client simple:

 virtualenv .env && source .env/bin/activate && \ pip install numpy grpcio opencv-python tensorflow tensorflow-serving-api 

Le modèle ResNet-50 v2 attend l'entrée de tenseurs à virgule flottante dans une structure de données formatée channels_last (NHWC). Par conséquent, l'image d'entrée est lue à l'aide d'opencv-python et chargée dans le tableau numpy (hauteur × largeur × canaux) en tant que type de données float32. Le script ci-dessous crée un talon de client de prédiction et charge les données JPEG dans un tableau numpy, les convertit en tensor_proto pour effectuer une demande de prévision pour gRPC:

 #!/usr/bin/env python from __future__ import print_function import argparse import numpy as np import time tt = time.time() import cv2 import tensorflow as tf from grpc.beta import implementations from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2 parser = argparse.ArgumentParser(description='incetion grpc client flags.') parser.add_argument('--host', default='0.0.0.0', help='inception serving host') parser.add_argument('--port', default='9000', help='inception serving port') parser.add_argument('--image', default='', help='path to JPEG image file') FLAGS = parser.parse_args() def main(): # create prediction service client stub channel = implementations.insecure_channel(FLAGS.host, int(FLAGS.port)) stub = prediction_service_pb2.beta_create_PredictionService_stub(channel) # create request request = predict_pb2.PredictRequest() request.model_spec.name = 'resnet' request.model_spec.signature_name = 'serving_default' # read image into numpy array img = cv2.imread(FLAGS.image).astype(np.float32) # convert to tensor proto and make request # shape is in NHWC (num_samples x height x width x channels) format tensor = tf.contrib.util.make_tensor_proto(img, shape=[1]+list(img.shape)) request.inputs['input'].CopyFrom(tensor) resp = stub.Predict(request, 30.0) print('total time: {}s'.format(time.time() - tt)) if __name__ == '__main__': main() 

Après avoir reçu une entrée JPEG, un client fonctionnel produira le résultat suivant:

 python tf_serving_client.py --image=images/pupper.jpg total time: 2.56152906418s 

Le tenseur résultant contient une prévision sous la forme d'une valeur entière et d'une probabilité de signes.

 outputs { key: "classes" value { dtype: DT_INT64 tensor_shape { dim { size: 1 } } int64_val: 238 } } outputs { key: "probabilities" ... 

Pour une seule demande, un tel délai n'est pas acceptable. Mais rien de surprenant: le binaire Tensorflow Serving est par défaut conçu pour la plus large gamme d'équipements pour la plupart des cas d'utilisation. Vous avez probablement remarqué les lignes suivantes dans les journaux du conteneur de service Tensorflow standard:

 I external/org_tensorflow/tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA 

Cela indique un binaire TensorFlow Serving exécuté sur une plate-forme CPU pour laquelle il n'a pas été optimisé.

Construisez un binaire optimisé


Selon la documentation de Tensorflow, il est recommandé de compiler Tensorflow à partir de la source avec toutes les optimisations disponibles pour le CPU sur l'hôte où le binaire fonctionnera. Lors de l'assemblage, des drapeaux spéciaux permettent l'activation des jeux d'instructions CPU pour une plate-forme spécifique:

Jeu d'instructionsDrapeaux
AVX--copt = -mavx
AVX2--copt = -mavx2
Fma--copt = -mfma
SSE 4.1--copt = -msse4.1
SSE 4.2--copt = -msse4.2
Tous pris en charge par le processeur--copt = -march = natif

Clonez un serveur Tensorflow d'une version spécifique. Dans notre cas, il s'agit de 1,13 (le dernier au moment de la publication de cet article):

 USER=$1 TAG=$2 TF_SERVING_VERSION_GIT_BRANCH="r1.13" git clone --branch="$TF_SERVING_VERSION_GIT_BRANCH" https://github.com/tensorflow/serving 

L'image de développement Tensorflow Serving utilise l'outil de Bâle pour créer. Nous le configurons pour des ensembles spécifiques d'instructions CPU:

 TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2" 

S'il n'y a pas assez de mémoire, limitez la consommation de mémoire pendant le processus de génération avec l'indicateur --local_resources=2048,.5,1.0 . Pour plus d'informations sur les indicateurs, consultez l' aide de Tensorflow Serving et Docker , ainsi que la documentation Bazel .

Créez une image de travail basée sur l'image existante:

 #!/bin/bash USER=$1 TAG=$2 TF_SERVING_VERSION_GIT_BRANCH="r1.13" git clone --branch="${TF_SERVING_VERSION_GIT_BRANCH}" https://github.com/tensorflow/serving TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2" cd serving && \ docker build --pull -t $USER/tensorflow-serving-devel:$TAG \ --build-arg TF_SERVING_VERSION_GIT_BRANCH="${TF_SERVING_VERSION_GIT_BRANCH}" \ --build-arg TF_SERVING_BUILD_OPTIONS="${TF_SERVING_BUILD_OPTIONS}" \ -f tensorflow_serving/tools/docker/Dockerfile.devel . cd serving && \ docker build -t $USER/tensorflow-serving:$TAG \ --build-arg TF_SERVING_BUILD_IMAGE=$USER/tensorflow-serving-devel:$TAG \ -f tensorflow_serving/tools/docker/Dockerfile . 

ModelServer est configuré à l'aide des indicateurs TensorFlow pour prendre en charge la concurrence. Les options suivantes configurent deux pools de threads pour un fonctionnement parallèle:

 intra_op_parallelism_threads 

  • contrĂ´le le nombre maximal de threads pour l'exĂ©cution parallèle d'une opĂ©ration;
  • utilisĂ© pour parallĂ©liser des opĂ©rations qui ont des sous-opĂ©rations de nature indĂ©pendante.

 inter_op_parallelism_threads 

  • contrĂ´le le nombre maximal de threads pour l'exĂ©cution parallèle d'opĂ©rations indĂ©pendantes;
  • Les opĂ©rations Tensorflow Graph, qui sont indĂ©pendantes les unes des autres et, par consĂ©quent, peuvent ĂŞtre effectuĂ©es dans diffĂ©rents threads.

Par défaut, les deux paramètres sont définis sur 0 . Cela signifie que le système lui-même sélectionne le nombre approprié, ce qui signifie le plus souvent un thread par cœur. Cependant, le paramètre peut être modifié manuellement pour la simultanéité multicœur.

Exécutez ensuite le conteneur de service de la même manière que le précédent, cette fois avec une image Docker compilée à partir des sources et avec des indicateurs d'optimisation Tensorflow pour un processeur spécifique:

 docker run -d -p 9000:8500 \ -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet \ -t $USER/tensorflow-serving:$TAG \ --tensorflow_intra_op_parallelism=4 \ --tensorflow_inter_op_parallelism=4 

Les journaux de conteneur ne devraient plus afficher d'avertissements concernant un processeur non défini. Sans changer le code sur la même demande de prévision, le retard est réduit d'environ 35,8%:

 python tf_serving_client.py --image=images/pupper.jpg total time: 1.64234706879s 

Accélérez la prévision des clients


Est-il encore possible d'accélérer? Nous avons optimisé le côté serveur pour notre CPU, mais un délai de plus d'une seconde semble encore trop important.

Il se trouve que le chargement des bibliothèques tensorflow_serving et tensorflow apporte une contribution significative au retard. Chaque appel inutile à tf.contrib.util.make_tensor_proto ajoute également une fraction de seconde.

Vous pouvez demander: "N'avons-nous pas besoin de packages TensorFlow Python pour faire des demandes de prédiction au serveur Tensorflow?" En fait, il n'y a pas vraiment besoin de tensorflow_serving et tensorflow .

Comme indiqué précédemment, les API de prédiction Tensorflow sont définies comme des proto-tampons. Par conséquent, deux dépendances externes peuvent être remplacées par les tensorflow et tensorflow_serving correspondants - et vous n'avez alors pas besoin d'extraire la bibliothèque Tensorflow entière (lourde) sur le client.

Tout d'abord, débarrassez-vous des tensorflow_serving tensorflow et tensorflow_serving et ajoutez le grpcio-tools .

 pip uninstall tensorflow tensorflow-serving-api && \ pip install grpcio-tools==1.0.0 

tensorflow/tensorflow les tensorflow/tensorflow et tensorflow/serving et copiez les fichiers protobuf suivants dans le projet client:

 tensorflow/serving/ tensorflow_serving/apis/model.proto tensorflow_serving/apis/predict.proto tensorflow_serving/apis/prediction_service.proto tensorflow/tensorflow/ tensorflow/core/framework/resource_handle.proto tensorflow/core/framework/tensor_shape.proto tensorflow/core/framework/tensor.proto tensorflow/core/framework/types.proto 

Copiez ces fichiers protobuf dans le répertoire protos/ avec les chemins d'origine préservés:

 protos/ tensorflow_serving/ apis/ *.proto tensorflow/ core/ framework/ *.proto 

Par souci de simplicité, prediction_service.proto peut être simplifié pour implémenter uniquement Predict RPC afin de ne pas télécharger les dépendances imbriquées des autres RPC spécifiés dans le service. Voici un exemple de prediction_service. simplifié.

Créez des implémentations gRPC Python à l'aide de grpcio.tools.protoc :

 PROTOC_OUT=protos/ PROTOS=$(find . | grep "\.proto$") for p in $PROTOS; do python -m grpc.tools.protoc -I . --python_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $p done 

Maintenant, l'ensemble tensorflow_serving module tensorflow_serving peut être supprimé:

 from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2 

... et remplacez par les protobuffers générés à partir de protos/tensorflow_serving/apis :

 from protos.tensorflow_serving.apis import predict_pb2 from protos.tensorflow_serving.apis import prediction_service_pb2 

La bibliothèque Tensorflow est importée pour utiliser la fonction d'assistance make_tensor_proto , qui est nécessaire pour encapsuler un objet python / numpy en tant qu'objet TensorProto.

Ainsi, nous pouvons remplacer la dépendance et le fragment de code suivants:

 import tensorflow as tf ... tensor = tf.contrib.util.make_tensor_proto(features) request.inputs['inputs'].CopyFrom(tensor) 

importer des protobuffers et construire un objet TensorProto:

 from protos.tensorflow.core.framework import tensor_pb2 from protos.tensorflow.core.framework import tensor_shape_pb2 from protos.tensorflow.core.framework import types_pb2 ... # ensure NHWC shape and build tensor proto tensor_shape = [1]+list(img.shape) dims = [tensor_shape_pb2.TensorShapeProto.Dim(size=dim) for dim in tensor_shape] tensor_shape = tensor_shape_pb2.TensorShapeProto(dim=dims) tensor = tensor_pb2.TensorProto( dtype=types_pb2.DT_FLOAT, tensor_shape=tensor_shape, float_val=list(img.reshape(-1))) request.inputs['inputs'].CopyFrom(tensor) 

Le script Python complet est ici . Exécutez un client de démarrage mis à jour qui effectue une demande de prédiction pour un service Tensorflow optimisé:

 python tf_inception_grpc_client.py --image=images/pupper.jpg total time: 0.58314920859s 

Le diagramme suivant montre le temps d'exécution prévu dans la version optimisée de Tensorflow Serving par rapport à la version standard, sur 10 exécutions:



Le retard moyen a diminué d'environ 3,38 fois.

Optimisation de la bande passante


Tensorflow Serving peut être configuré pour gérer de grandes quantités de données. L'optimisation de la bande passante est généralement effectuée pour le traitement par lots «autonome», où les limites de latence serrées ne sont pas une exigence stricte.

Traitement par lots côté serveur


Comme indiqué dans la documentation , le traitement par lots côté serveur est nativement pris en charge dans Tensorflow Serving.

Les compromis entre latence et débit sont déterminés par les paramètres de traitement par lots. Ils vous permettent d'atteindre le débit maximal dont les accélérateurs matériels sont capables.

Pour activer l'empaquetage, définissez les --batching_parameters_file --enable_batching et --batching_parameters_file . Les paramètres sont définis en fonction de SessionBundleConfig . Pour les systèmes sur le CPU, définissez num_batch_threads sur le nombre de cœurs disponibles. Pour le GPU, voir les paramètres appropriés ici .

Après avoir rempli l'intégralité du package côté serveur, les demandes d'émission sont combinées en une seule grande requête (tenseur) et envoyées à la session Tensorflow avec une requête combinée. Dans cette situation, le parallélisme CPU / GPU est vraiment impliqué.

Quelques utilisations courantes du traitement par lots Tensorflow:

  • Utilisation de demandes de clients asynchrones pour remplir des paquets cĂ´tĂ© serveur
  • Traitement par lots plus rapide en transfĂ©rant les composants du graphique du modèle vers le CPU / GPU
  • Traitement des demandes de plusieurs modèles Ă  partir d'un seul serveur
  • Le traitement par lots est fortement recommandĂ© pour le traitement "hors ligne" d'un grand nombre de demandes

Traitement par lots côté client


Le traitement par lots côté client regroupe plusieurs demandes entrantes en une seule.

Étant donné que le modèle ResNet attend une entrée au format NHWC (la première dimension est le nombre d'entrées), nous pouvons combiner plusieurs images d'entrée en une seule demande RPC:

 ... batch = [] for jpeg in os.listdir(FLAGS.images_path): path = os.path.join(FLAGS.images_path, jpeg) img = cv2.imread(path).astype(np.float32) batch.append(img) ... batch_np = np.array(batch).astype(np.float32) dims = [tensor_shape_pb2.TensorShapeProto.Dim(size=dim) for dim in batch_np.shape] t_shape = tensor_shape_pb2.TensorShapeProto(dim=dims) tensor = tensor_pb2.TensorProto( dtype=types_pb2.DT_FLOAT, tensor_shape=t_shape, float_val=list(batched_np.reshape(-1))) request.inputs['inputs'].CopyFrom(tensor) 

Pour un paquet de N images, le tenseur de sortie dans la réponse contiendra les résultats de prédiction pour le même nombre d'entrées. Dans notre cas, N = 2:

 outputs { key: "classes" value { dtype: DT_INT64 tensor_shape { dim { size: 2 } } int64_val: 238 int64_val: 121 } } ... 

Accélération matérielle


Quelques mots sur les GPU.

Le processus d'apprentissage utilise naturellement la parallélisation sur le GPU, car la construction de réseaux de neurones profonds nécessite des calculs massifs pour obtenir la solution optimale.

Mais pour produire des résultats, la parallélisation n'est pas si évidente. Souvent, vous pouvez accélérer la sortie d'un réseau de neurones vers un GPU, mais vous devez sélectionner et tester soigneusement l'équipement, et effectuer une analyse technique et économique approfondie. La parallélisation matérielle est plus utile pour le traitement par lots de conclusions "autonomes" (volumes massifs).

Avant de passer à un GPU, tenez compte des besoins de l'entreprise avec une analyse minutieuse des coûts (monétaires, opérationnels, techniques) pour le plus grand avantage (latence réduite, bande passante élevée).

Source: https://habr.com/ru/post/fr445928/


All Articles