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:
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:
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:
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 ...
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).