Cómo aumentamos la productividad de servicio de Tensorflow en un 70%

Tensorflow se ha convertido en la plataforma estándar para el aprendizaje automático (ML), popular tanto en la industria como en la investigación. Se han creado muchas bibliotecas, herramientas y marcos gratuitos para capacitar y mantener modelos de ML. El proyecto de Tensorflow Serving ayuda a mantener los modelos ML en un entorno de producción distribuido.

Nuestro servicio Mux utiliza Tensorflow Serving en varias partes de la infraestructura, ya hemos discutido el uso de Tensorflow Serving en la codificación de títulos de video. Hoy nos centraremos en métodos que mejoran la latencia optimizando tanto el servidor de pronóstico como el cliente. Los pronósticos de modelos suelen ser operaciones "en línea" (en la ruta crítica de solicitud de una aplicación), por lo tanto, los objetivos principales de la optimización son procesar grandes volúmenes de solicitudes con el menor retraso posible.

¿Qué es el servicio Tensorflow?


Tensorflow Serving proporciona una arquitectura de servidor flexible para implementar y mantener modelos ML. Una vez que el modelo está entrenado y listo para ser utilizado para el pronóstico, Tensorflow Serving requiere exportarlo a un formato compatible (de servicio).

Servable es una abstracción central que envuelve los objetos de Tensorflow. Por ejemplo, un modelo se puede representar como uno o más objetos Servable. Por lo tanto, Servable son los objetos básicos que el cliente usa para realizar cálculos. El tamaño de servicio es importante: los modelos más pequeños ocupan menos espacio, usan menos memoria y se cargan más rápido. Para descargar y mantener utilizando la API de predicción, los modelos deben estar en formato de modelo guardado.



Tensorflow Serving combina los componentes básicos para crear un servidor gRPC / HTTP que sirve a varios modelos ML (o varias versiones), proporciona componentes de monitoreo y una arquitectura personalizada.

Tensorflow Sirviendo con Docker


Echemos un vistazo a las métricas básicas de latencia en el pronóstico del rendimiento con la configuración estándar de Servidor de Tensorflow (sin optimización de CPU).

Primero, descargue la última imagen del centro TensorFlow Docker:

docker pull tensorflow/serving:latest 

En este artículo, todos los contenedores se ejecutan en un host con cuatro núcleos, 15 GB, Ubuntu 16.04.

Exportar modelo de Tensorflow a modelo guardado


Cuando un modelo se entrena utilizando Tensorflow, la salida se puede guardar como puntos de control variables (archivos en el disco). La salida se realiza directamente restaurando los puntos de control del modelo o en un formato de gráfico congelado congelado (archivo binario).

Para el servicio Tensorflow, este gráfico congelado debe exportarse al formato GuardadoModelo. La documentación de Tensorflow contiene ejemplos de exportación de modelos entrenados al formato SavedModel.

Tensorflow también proporciona muchos modelos oficiales y de investigación como punto de partida para la experimentación, la investigación o la producción.

Como ejemplo, utilizaremos el modelo de red neuronal residual profunda (ResNet) para clasificar un conjunto de datos ImageNet de 1000 clases. Descargue el modelo pre - ResNet-50 v2 , específicamente la opción Channels_last (NHWC) en SavedModel : como regla, funciona mejor en la CPU.

Copie el directorio del modelo RestNet en la siguiente estructura:

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

Tensorflow Serving espera una estructura de directorio ordenada numéricamente para el control de versiones. En nuestro caso, el directorio 1/ corresponde al modelo de la versión 1, que contiene la arquitectura del modelo saved_model.pb con una instantánea de los pesos del modelo (variables).

Cargando y procesando el modelo guardado


El siguiente comando inicia el servidor modelo Tensorflow Serving en un contenedor Docker. Para cargar SavedModel, debe montar el directorio del modelo en el directorio del contenedor esperado.

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

La comprobación de los registros del contenedor muestra que ModelServer está en funcionamiento para manejar las solicitudes de salida para el modelo de resnet en los puntos finales gRPC y 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 ... 

Cliente de pronóstico


Tensorflow Serving define un esquema de API en formato de buffers de protocolo (protobufs). Las implementaciones de cliente GRPC para la API de pronóstico se empaquetan como un paquete de Python tensorflow_serving.apis . Necesitaremos otro paquete de tensorflow de Python para funciones de utilidad.

Instale las dependencias para crear un cliente simple:

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

El modelo ResNet-50 v2 espera la entrada de tensores de coma flotante en una estructura de datos formateada channel_last (NHWC). Por lo tanto, la imagen de entrada se lee usando opencv-python y se carga en la matriz numpy (alto × ancho × canales) como un tipo de datos float32. El siguiente script crea un código auxiliar de cliente de predicción y carga los datos JPEG en una matriz numpy, los convierte en tensor_proto para hacer una solicitud de pronóstico para 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() 

Habiendo recibido una entrada JPEG, un cliente que trabaja producirá el siguiente resultado:

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

El tensor resultante contiene un pronóstico en forma de valor entero y probabilidad de signos.

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

Para una sola solicitud, dicho retraso no es aceptable. Pero nada sorprendente: el binario Tensorflow Serving está diseñado por defecto para la gama más amplia de equipos para la mayoría de los casos de uso. Probablemente haya notado las siguientes líneas en los registros del contenedor estándar de Tensorflow Serving:

 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 

Esto indica un binario de TensorFlow Serving que se ejecuta en una plataforma de CPU para la que no ha sido optimizado.

Construye un binario optimizado


De acuerdo con la documentación de Tensorflow, se recomienda compilar Tensorflow desde la fuente con todas las optimizaciones disponibles para la CPU en el host donde funcionará el binario. Al ensamblar, los indicadores especiales permiten la activación de conjuntos de instrucciones de CPU para una plataforma específica:

Conjunto de instruccionesBanderas
AVX--copt = -mavx
AVX2--copt = -mavx2
Fma--copt = -mfma
SSE 4.1--copt = -msse4.1
SSE 4.2--copt = -msse4.2
Todo soportado por procesador--copt = -march = nativo

Clonar un Tensorflow Sirviendo de una versión específica. En nuestro caso, esto es 1.13 (el último en el momento de la publicación de este artículo):

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

La imagen de desarrollo de Tensorflow Serving utiliza la herramienta de Basilea para construir. Lo configuramos para conjuntos específicos de instrucciones de CPU:

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

Si no hay suficiente memoria, limite el consumo de memoria durante el proceso de compilación con el indicador --local_resources=2048,.5,1.0 . Para obtener información sobre indicadores, consulte la ayuda de Tensorflow Serving y Docker , así como la documentación de Bazel .

Cree una imagen de trabajo basada en la existente:

 #!/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 se configura utilizando indicadores TensorFlow para admitir concurrencia. Las siguientes opciones configuran dos grupos de subprocesos para la operación en paralelo:

 intra_op_parallelism_threads 

  • controla el número máximo de subprocesos para la ejecución paralela de una operación;
  • se usa para paralelizar operaciones que tienen suboperaciones que son de naturaleza independiente.

 inter_op_parallelism_threads 

  • controla el número máximo de subprocesos para la ejecución paralela de operaciones independientes;
  • Las operaciones de Tensorflow Graph, que son independientes entre sí y, por lo tanto, se pueden realizar en diferentes subprocesos.

Por defecto, ambos parámetros están establecidos en 0 . Esto significa que el sistema mismo selecciona el número apropiado, lo que a menudo significa un subproceso por núcleo. Sin embargo, el parámetro se puede cambiar manualmente para la concurrencia de múltiples núcleos.

Luego, ejecute el contenedor Serving de la misma manera que el anterior, esta vez con una imagen Docker compilada de las fuentes y con indicadores de optimización de Tensorflow para un procesador específico:

 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 

Los registros de contenedores ya no deberían mostrar advertencias sobre una CPU indefinida. Sin cambiar el código en la misma solicitud de pronóstico, el retraso se reduce en aproximadamente un 35,8%:

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

Aumente la velocidad en el pronóstico del cliente.


¿Todavía es posible acelerar? Hemos optimizado el lado del servidor para nuestra CPU, pero un retraso de más de 1 segundo todavía parece demasiado grande.

Dio la casualidad de que cargar las bibliotecas tensorflow_serving y tensorflow hace una contribución significativa al retraso. Cada llamada innecesaria a tf.contrib.util.make_tensor_proto también agrega una fracción de segundo.

Puede preguntar: "¿No necesitamos paquetes TensorFlow Python para hacer solicitudes de predicción al servidor Tensorflow?" De hecho, no hay una necesidad real de paquetes tensorflow_serving y tensorflow .

Como se señaló anteriormente, las API de predicción de Tensorflow se definen como proto-buffers. Por lo tanto, se pueden reemplazar dos dependencias externas con los tensorflow y tensorflow_serving correspondientes, y luego no es necesario extraer toda la biblioteca (pesada) de Tensorflow en el cliente.

Primero, elimine las tensorflow_serving tensorflow y tensorflow_serving y agregue el paquete grpcio-tools .

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

Clone los tensorflow/tensorflow y tensorflow/serving y copie los siguientes archivos protobuf en el proyecto del cliente:

 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 

Copie estos archivos protobuf en el directorio protos/ con las rutas originales conservadas:

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

Por simplicidad, prediction_service.proto puede simplificarse para implementar solo Predict RPC para no descargar las dependencias anidadas de otros RPC especificados en el servicio. Aquí hay un ejemplo de prediction_service. simplificado.

Cree implementaciones de Python gRPC utilizando 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 

Ahora se puede eliminar todo el módulo tensorflow_serving :

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

... y reemplace con los protobuffers generados desde protos/tensorflow_serving/apis :

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

La biblioteca Tensorflow se importa para usar la función auxiliar make_tensor_proto , que es necesaria para envolver un objeto python / numpy como un objeto TensorProto.

Por lo tanto, podemos reemplazar la siguiente dependencia y fragmento de código:

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

importar protobuffers y construir un objeto 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) 

El script completo de Python está aquí . Ejecute un cliente de inicio actualizado que realice una solicitud de predicción para el servicio optimizado de Tensorflow:

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

El siguiente diagrama muestra el tiempo de ejecución del pronóstico en la versión optimizada de Tensorflow Serving en comparación con el estándar, más de 10 ejecuciones:



El retraso promedio disminuyó en aproximadamente 3.38 veces.

Optimización de ancho de banda


Tensorflow Serving se puede configurar para manejar grandes cantidades de datos. La optimización del ancho de banda generalmente se realiza para el procesamiento por lotes "autónomo", donde los límites de latencia ajustados no son un requisito estricto.

Procesamiento por lotes del lado del servidor


Como se indica en la documentación , el procesamiento por lotes del lado del servidor es compatible de forma nativa en Tensorflow Serving.

Las compensaciones entre latencia y rendimiento se determinan mediante parámetros de procesamiento por lotes. Le permiten alcanzar el rendimiento máximo que los aceleradores de hardware son capaces de hacer.

Para habilitar el empaquetado, configure los --batching_parameters_file --enable_batching y --batching_parameters_file . Los parámetros se establecen de acuerdo con SessionBundleConfig . Para sistemas en la CPU, establezca num_batch_threads en el número de núcleos disponibles. Para la GPU, vea los parámetros apropiados aquí .

Después de completar todo el paquete en el lado del servidor, las solicitudes de emisión se combinan en una solicitud grande (tensor) y se envían a la sesión de Tensorflow con una solicitud combinada. En esta situación, el paralelismo CPU / GPU está realmente involucrado.

Algunos usos comunes para el procesamiento por lotes de Tensorflow:

  • Uso de solicitudes de cliente asíncronas para llenar paquetes del lado del servidor
  • Procesamiento por lotes más rápido mediante la transferencia de los componentes del gráfico del modelo a la CPU / GPU
  • Sirviendo solicitudes de múltiples modelos desde un único servidor
  • El procesamiento por lotes es muy recomendable para el procesamiento "fuera de línea" de una gran cantidad de solicitudes

Procesamiento por lotes del lado del cliente


El procesamiento por lotes del lado del cliente agrupa varias solicitudes entrantes en una.

Dado que el modelo ResNet está esperando entrada en formato NHWC (la primera dimensión es la cantidad de entradas), podemos combinar varias imágenes de entrada en una solicitud 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) 

Para un paquete de N imágenes, el tensor de salida en la respuesta contendrá los resultados de predicción para el mismo número de entradas. En nuestro caso, N = 2:

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

Aceleración de hardware


Algunas palabras sobre las GPU.

El proceso de aprendizaje utiliza naturalmente la paralelización en la GPU, ya que la construcción de redes neuronales profundas requiere cálculos masivos para lograr la solución óptima.

Pero para generar resultados, la paralelización no es tan obvia. A menudo, puede acelerar la salida de una red neuronal a una GPU, pero debe seleccionar y probar cuidadosamente el equipo y realizar un análisis técnico y económico en profundidad. La paralelización de hardware es más valiosa para el procesamiento por lotes de conclusiones "autónomas" (volúmenes masivos).

Antes de pasar a una GPU, considere los requisitos comerciales con un análisis cuidadoso de los costos (monetario, operativo, técnico) para obtener el mayor beneficio (latencia reducida, alto rendimiento).

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


All Articles