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