Como aumentamos a produtividade do serviço em Tensorflow em 70%

O Tensorflow se tornou a plataforma padrão para aprendizado de máquina (ML), popular tanto no setor quanto na pesquisa. Muitas bibliotecas, ferramentas e estruturas gratuitas foram criadas para treinamento e manutenção de modelos de ML. O projeto Tensorflow Serving ajuda a manter modelos de ML em um ambiente de produção distribuído.

Nosso serviço Mux usa o Tensorflow Serving em várias partes da infraestrutura. Já discutimos o uso do Tensorflow Serving na codificação de títulos de vídeo. Hoje, focaremos nos métodos que melhoram a latência, otimizando o servidor de previsão e o cliente. As previsões de modelo são geralmente operações "online" (no caminho crítico da solicitação de um aplicativo); portanto, os principais objetivos da otimização são processar grandes volumes de solicitações com o menor atraso possível.

O que é o Tensorflow Serving?


O Tensorflow Serving fornece uma arquitetura de servidor flexível para implantar e manter modelos de ML. Depois que o modelo é treinado e pronto para uso para previsão, o Tensorflow Serving exige exportá-lo para um formato compatível (que pode ser utilizado).

Servable é uma abstração central que envolve objetos do Tensorflow. Por exemplo, um modelo pode ser representado como um ou mais objetos Servíveis. Assim, Servable são os objetos básicos que o cliente usa para executar cálculos. O tamanho do serviço é importante: modelos menores ocupam menos espaço, usam menos memória e carregam mais rapidamente. Para baixar e manter usando a API Predict, os modelos devem estar no formato SavedModel.



O Tensorflow Serving combina os componentes básicos para criar um servidor gRPC / HTTP que atende a vários modelos de ML (ou várias versões), fornece componentes de monitoramento e uma arquitetura customizada.

Serviço Tensorflow com Docker


Vamos dar uma olhada nas métricas básicas de latência na previsão de desempenho com as configurações padrão do Tensorflow Serving (sem otimização da CPU).

Primeiro, baixe a imagem mais recente do hub TensorFlow Docker:

docker pull tensorflow/serving:latest 

Neste artigo, todos os contêineres são executados em um host com quatro núcleos, 15 GB, Ubuntu 16.04.

Exportar modelo de fluxo de tensão para SavedModel


Quando um modelo é treinado usando o Tensorflow, a saída pode ser salva como pontos de controle variáveis ​​(arquivos em disco). A saída é realizada diretamente, restaurando os pontos de controle do modelo ou em um formato de gráfico congelado congelado (arquivo binário).

Para a Tensorflow Serving, esse gráfico congelado precisa ser exportado para o formato SavedModel. A documentação do Tensorflow contém exemplos de exportação de modelos treinados para o formato SavedModel.

O Tensorflow também fornece muitos modelos oficiais e de pesquisa como ponto de partida para experimentação, pesquisa ou produção.

Como exemplo, usaremos o modelo de rede neural residual profunda (ResNet) para classificar um conjunto de dados ImageNet de 1000 classes. Faça o download do modelo ResNet-50 v2 pré - ResNet-50 v2 , especificamente a opção Channels_last (NHWC) no SavedModel : como regra geral, ele funciona melhor na CPU.

Copie o diretório do modelo RestNet na seguinte estrutura:

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

O Tensorflow Serving espera uma estrutura de diretórios ordenada numericamente para controle de versão. No nosso caso, o diretório 1/ corresponde ao modelo da versão 1, que contém a arquitetura do modelo saved_model.pb com uma captura instantânea dos pesos do modelo (variáveis).

Carregando e processando SavedModel


O comando a seguir inicia o servidor do modelo Tensorflow Serving em um contêiner do Docker. Para carregar SavedModel, você deve montar o diretório de modelo no diretório de contêiner esperado.

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

A verificação dos logs do contêiner mostra que o ModelServer está ativo e em execução para manipular solicitações de saída para o modelo de resnet - resnet nos pontos de extremidade gRPC e 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 Previsão


O Tensorflow Serving define um esquema de API no formato de buffers de protocolo (protobufs). As implementações do cliente GRPC para a API de previsão são empacotadas como um pacote Python tensorflow_serving.apis . Nós precisaremos de outro pacote Python tensorflow para funções utilitárias.

Instale as dependências para criar um cliente simples:

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

O modelo ResNet-50 v2 espera a entrada de tensores de ponto flutuante em uma estrutura de dados formatados channels_last (NHWC). Portanto, a imagem de entrada é lida usando opencv-python e carregada na matriz numpy (altura × largura × canais) como um tipo de dados float32. O script abaixo cria um stub do cliente de previsão e carrega os dados JPEG em uma matriz numpy, converte-os em tensor_proto para fazer uma solicitação de previsão para o 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() 

Após receber uma entrada JPEG, um cliente ativo produzirá o seguinte resultado:

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

O tensor resultante contém uma previsão na forma de um valor inteiro e probabilidade de sinais.

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

Para uma única solicitação, esse atraso não é aceitável. Mas nada de surpreendente: o binário Tensorflow Serving é projetado por padrão para a mais ampla gama de equipamentos para a maioria dos casos de uso. Você provavelmente notou as seguintes linhas nos logs do contêiner Tensorflow Serving padrão:

 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 

Isso indica um binário do TensorFlow Serving em execução em uma plataforma de CPU para a qual não foi otimizado.

Construa um binário otimizado


De acordo com a documentação do Tensorflow, é recomendável compilar o Tensorflow a partir da fonte com todas as otimizações disponíveis para a CPU no host em que o binário funcionará. Ao montar, sinalizadores especiais permitem a ativação de conjuntos de instruções da CPU para uma plataforma específica:

Conjunto de instruçõesBandeiras
AVX--copt = -mavx
AVX2--copt = -mavx2
Fma--copt = -mfma
SSE 4.1--copt = -msse4.1
SSE 4.2--copt = -msse4.2
Tudo suportado pelo processador--copt = -march = nativo

Clone uma veiculação de fluxo tensor de uma versão específica. No nosso caso, é 1,13 (o último no momento da publicação deste artigo):

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

A imagem do desenvolvedor do Tensorflow Serving usa a ferramenta Basel para criar. Nós o configuramos para conjuntos específicos de instruções da CPU:

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

Se não houver memória suficiente, limite o consumo de memória durante o processo de construção com o sinalizador --local_resources=2048,.5,1.0 . Para obter informações sobre sinalizadores, consulte a ajuda do Tensorflow Serving and Docker , bem como a documentação do Bazel .

Crie uma imagem de trabalho com base na 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 . 

O ModelServer é configurado usando sinalizadores TensorFlow para oferecer suporte à simultaneidade. As seguintes opções configuram dois conjuntos de encadeamentos para operação paralela:

 intra_op_parallelism_threads 

  • controla o número máximo de threads para execução paralela de uma operação;
  • usado para paralelizar operações que possuem suboperações de natureza independente.

 inter_op_parallelism_threads 

  • controla o número máximo de threads para execução paralela de operações independentes;
  • As operações de gráfico de tensão tensor, que são independentes uma da outra e, portanto, podem ser executadas em threads diferentes.

Por padrão, os dois parâmetros são definidos como 0 . Isso significa que o próprio sistema seleciona o número apropriado, o que geralmente significa um encadeamento por núcleo. No entanto, o parâmetro pode ser alterado manualmente para simultaneidade de vários núcleos.

Em seguida, execute o contêiner Serving da mesma maneira que o anterior, desta vez com uma imagem do Docker compilada a partir das fontes e com os sinalizadores de otimização do Tensorflow para um processador 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 

Os logs do contêiner não devem mais mostrar avisos sobre uma CPU indefinida. Sem alterar o código na mesma solicitação de previsão, o atraso é reduzido em cerca de 35,8%:

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

Aumentar a velocidade na previsão do cliente


Ainda é possível acelerar? Otimizamos o lado do servidor para nossa CPU, mas um atraso de mais de 1 segundo ainda parece muito grande.

Aconteceu que o carregamento das bibliotecas tensorflow e tensorflow contribui significativamente para o atraso. Cada chamada desnecessária para tf.contrib.util.make_tensor_proto também adiciona uma fração de segundo.

Você pode perguntar: "Não precisamos dos pacotes TensorFlow Python para realmente fazer solicitações de previsão ao servidor Tensorflow?" De fato, não há necessidade real de pacotes tensorflow e tensorflow .

Conforme observado anteriormente, as APIs de previsão do Tensorflow são definidas como proto-buffers. Portanto, duas dependências externas podem ser substituídas tensorflow_serving correspondentes tensorflow e tensorflow_serving - e você não precisa extrair toda a biblioteca (pesada) do Tensorflow no cliente.

Primeiro, livre-se das tensorflow_serving e tensorflow_serving e adicione o pacote grpcio-tools .

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

Clone os tensorflow/tensorflow e tensorflow/serving e copie os seguintes arquivos protobuf no projeto do 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 esses arquivos protobuf para o diretório protos/ com os caminhos originais preservados:

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

Por uma questão de simplicidade, o prediction_service.proto pode ser simplificado para implementar apenas o Predict RPC, para não baixar as dependências aninhadas de outros RPCs especificados no serviço. Aqui está um exemplo de um prediction_service. simplificado.

Crie implementações de gRPC do Python usando 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 

Agora, todo o módulo tensorflow_serving pode ser removido:

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

... e substitua pelos protobuffers gerados a partir de protos/tensorflow_serving/apis :

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

A biblioteca Tensorflow é importada para usar a função auxiliar make_tensor_proto , necessária para agrupar um objeto python / numpy como um objeto TensorProto.

Assim, podemos substituir a seguinte dependência e fragmento de código:

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

importar protobuffers e criar um 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) 

O script Python completo está aqui . Execute um cliente inicial atualizado que faça uma solicitação de previsão para a exibição otimizada do Tensorflow:

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

O diagrama a seguir mostra o tempo de execução da previsão na versão otimizada do Tensorflow Serving em comparação com o padrão, em 10 execuções:



O atraso médio diminuiu cerca de 3,38 vezes.

Otimização da largura de banda


O Tensorflow Serving pode ser configurado para lidar com grandes quantidades de dados. A otimização da largura de banda geralmente é executada para processamento em lote "independente", onde limites de latência rígidos não são um requisito estrito.

Processamento em lote do lado do servidor


Conforme declarado na documentação , o processamento em lote do lado do servidor é suportado nativamente no Tensorflow Serving.

As compensações entre latência e taxa de transferência são determinadas pelos parâmetros de processamento em lote. Eles permitem que você alcance a taxa de transferência máxima que os aceleradores de hardware são capazes.

Para ativar o empacotamento, defina os --batching_parameters_file --enable_batching e --batching_parameters_file . Os parâmetros são definidos de acordo com SessionBundleConfig . Para sistemas na CPU, configure num_batch_threads para o número de núcleos disponíveis. Para a GPU, consulte os parâmetros apropriados aqui .

Após preencher o pacote inteiro no lado do servidor, as solicitações de emissão são combinadas em uma solicitação grande (tensor) e enviadas à sessão do Tensorflow com uma solicitação combinada. Nessa situação, o paralelismo CPU / GPU está realmente envolvido.

Alguns usos comuns para o processamento em lote do Tensorflow:

  • Usando solicitações de cliente assíncronas para preencher pacotes do lado do servidor
  • Processamento em lote mais rápido, transferindo os componentes do gráfico do modelo para a CPU / GPU
  • Atendendo solicitações de vários modelos em um único servidor
  • O processamento em lote é altamente recomendado para o processamento "offline" de um grande número de solicitações

Processamento em lote do lado do cliente


O processamento em lote do lado do cliente agrupa várias solicitações recebidas em uma.

Como o modelo ResNet está aguardando entrada no formato NHWC (a primeira dimensão é o número de entradas), podemos combinar várias imagens de entrada em uma solicitação 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 um pacote de N imagens, o tensor de saída na resposta conterá os resultados da previsão para o mesmo número de entradas. No nosso caso, N = 2:

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

Aceleração de hardware


Algumas palavras sobre GPUs.

O processo de aprendizado naturalmente usa paralelização na GPU, pois a construção de redes neurais profundas exige cálculos massivos para alcançar a solução ideal.

Mas, para gerar resultados, a paralelização não é tão óbvia. Freqüentemente, você pode acelerar a saída de uma rede neural para uma GPU, mas precisa selecionar e testar cuidadosamente o equipamento, além de realizar análises técnicas e econômicas detalhadas. A paralelização de hardware é mais valiosa para o processamento em lote de conclusões "autônomas" (grandes volumes).

Antes de mudar para uma GPU, considere os requisitos de negócios com uma análise cuidadosa dos custos (monetário, operacional, técnico) para obter o maior benefício (latência reduzida, alto rendimento).

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


All Articles