Construindo um ModelServer padrão do TensorFlow

Este tutorial mostra como usar componentes do TensorFlow Serving para criar o TensorFlow ModelServer padrão que descobre e fornece dinamicamente novas versões de um modelo treinado do TensorFlow. Se você quiser apenas usar o servidor padrão para servir seus modelos, consulte Tutorial básico do TensorFlow Serving .

Este tutorial usa o modelo simples de regressão Softmax apresentado no tutorial do TensorFlow para classificação de imagens manuscritas (dados MNIST). Se você não sabe o que é TensorFlow ou MNIST, consulte o tutorial MNIST para iniciantes em ML .

O código deste tutorial consiste em duas partes:

  • Um arquivo Python mnist_saved_model.py que treina e exporta diversas versões do modelo.

  • Um arquivo C++ main.cc que é o TensorFlow ModelServer padrão que descobre novos modelos exportados e executa um serviço gRPC para atendê-los.

Este tutorial percorre as seguintes tarefas:

  1. Treine e exporte um modelo do TensorFlow.
  2. Gerencie o controle de versão do modelo com o TensorFlow Serving ServerCore .
  3. Configure o lote usando SavedModelBundleSourceAdapterConfig .
  4. Servir solicitação com TensorFlow Serving ServerCore .
  5. Execute e teste o serviço.

Antes de começar, primeiro instale o Docker

Treinar e exportar modelo do TensorFlow

Primeiro, se ainda não fez isso, clone este repositório em sua máquina local:

git clone https://github.com/tensorflow/serving.git
cd serving

Limpe o diretório de exportação se ele já existir:

rm -rf /tmp/models

Treine (com 100 iterações) e exporte a primeira versão do modelo:

tools/run_in_docker.sh python tensorflow_serving/example/mnist_saved_model.py \
  --training_iteration=100 --model_version=1 /tmp/mnist

Treine (com 2.000 iterações) e exporte a segunda versão do modelo:

tools/run_in_docker.sh python tensorflow_serving/example/mnist_saved_model.py \
  --training_iteration=2000 --model_version=2 /tmp/mnist

Como você pode ver em mnist_saved_model.py , o treinamento e a exportação são feitos da mesma forma que no tutorial básico do TensorFlow Serving . Para fins de demonstração, você está intencionalmente diminuindo as iterações de treinamento para a primeira execução e exportando-as como v1, enquanto treina normalmente para a segunda execução e exportando-as como v2 para o mesmo diretório pai - como esperamos que o último alcance melhor precisão de classificação devido ao treinamento mais intensivo. Você deverá ver os dados de treinamento para cada execução de treinamento em seu diretório /tmp/mnist :

$ ls /tmp/mnist
1  2

ServerCore

Agora imagine que v1 e v2 do modelo sejam gerados dinamicamente em tempo de execução, à medida que novos algoritmos estão sendo experimentados ou à medida que o modelo é treinado com um novo conjunto de dados. Em um ambiente de produção, você pode querer construir um servidor que possa suportar implementação gradual, no qual a v2 possa ser descoberta, carregada, experimentada, monitorada ou revertida enquanto atende a v1. Alternativamente, você pode querer desmontar a v1 antes de abrir a v2. O TensorFlow Serving oferece suporte a ambas as opções: enquanto uma é boa para manter a disponibilidade durante a transição, a outra é boa para minimizar o uso de recursos (por exemplo, RAM).

O TensorFlow Serving Manager faz exatamente isso. Ele lida com todo o ciclo de vida dos modelos do TensorFlow, incluindo carregamento, veiculação e descarregamento, bem como transições de versão. Neste tutorial, você criará seu servidor sobre um TensorFlow Serving ServerCore , que encapsula internamente um AspiredVersionsManager .

int main(int argc, char** argv) {
  ...

  ServerCore::Options options;
  options.model_server_config = model_server_config;
  options.servable_state_monitor_creator = &CreateServableStateMonitor;
  options.custom_model_config_loader = &LoadCustomModelConfig;

  ::google::protobuf::Any source_adapter_config;
  SavedModelBundleSourceAdapterConfig
      saved_model_bundle_source_adapter_config;
  source_adapter_config.PackFrom(saved_model_bundle_source_adapter_config);
  (*(*options.platform_config_map.mutable_platform_configs())
      [kTensorFlowModelPlatform].mutable_source_adapter_config()) =
      source_adapter_config;

  std::unique_ptr<ServerCore> core;
  TF_CHECK_OK(ServerCore::Create(options, &core));
  RunServer(port, std::move(core));

  return 0;
}

ServerCore::Create() usa um parâmetro ServerCore::Options. Aqui estão algumas opções comumente usadas:

  • ModelServerConfig que especifica modelos a serem carregados. Os modelos são declarados por meio de model_config_list , que declara uma lista estática de modelos, ou por meio de custom_model_config , que define uma maneira personalizada de declarar uma lista de modelos que podem ser atualizados em tempo de execução.
  • PlatformConfigMap que mapeia do nome da plataforma (como tensorflow ) para PlatformConfig , que é usado para criar o SourceAdapter . SourceAdapter adapta StoragePath (o caminho onde uma versão do modelo é descoberta) ao model Loader (carrega a versão do modelo do caminho de armazenamento e fornece interfaces de transição de estado para o Manager ). Se PlatformConfig contiver SavedModelBundleSourceAdapterConfig , será criado um SavedModelBundleSourceAdapter , que explicaremos mais tarde.

SavedModelBundle é um componente chave do TensorFlow Serving. Ele representa um modelo do TensorFlow carregado de um determinado caminho e fornece a mesma Session::Run que o TensorFlow para executar a inferência. SavedModelBundleSourceAdapter adapta o caminho de armazenamento para Loader<SavedModelBundle> para que o tempo de vida do modelo possa ser gerenciado por Manager . Observe que SavedModelBundle é o sucessor do obsoleto SessionBundle . Os usuários são incentivados a usar SavedModelBundle pois o suporte para SessionBundle será removido em breve.

Com tudo isso, ServerCore faz internamente o seguinte:

  • Instancia um FileSystemStoragePathSource que monitora os caminhos de exportação do modelo declarados em model_config_list .
  • Instancia um SourceAdapter usando PlatformConfigMap com a plataforma de modelo declarada em model_config_list e conecta FileSystemStoragePathSource a ele. Dessa forma, sempre que uma nova versão do modelo é descoberta no caminho de exportação, o SavedModelBundleSourceAdapter a adapta para um Loader<SavedModelBundle> .
  • Instancia uma implementação específica do Manager chamada AspiredVersionsManager que gerencia todas as instâncias Loader criadas pelo SavedModelBundleSourceAdapter . ServerCore exporta a interface Manager delegando as chamadas para AspiredVersionsManager .

Sempre que uma nova versão estiver disponível, este AspiredVersionsManager carrega a nova versão e, sob seu comportamento padrão, descarrega a antiga. Se quiser começar a personalizar, recomendamos que você entenda os componentes criados internamente e como configurá-los.

Vale ressaltar que o TensorFlow Serving foi projetado do zero para ser muito flexível e extensível. Você pode construir vários plug-ins para personalizar o comportamento do sistema, enquanto aproveita os componentes principais genéricos como ServerCore e AspiredVersionsManager . Por exemplo, você pode criar um plug-in de fonte de dados que monitore o armazenamento em nuvem em vez do armazenamento local, ou pode criar um plug-in de política de versão que faça a transição de versão de uma maneira diferente. Na verdade, você pode até criar um plug-in de modelo personalizado que atenda modelos não TensorFlow. Esses tópicos estão fora do escopo deste tutorial. No entanto, você pode consultar os tutoriais de origem customizada e de serviço customizado para obter mais informações.

Lote

Outro recurso típico de servidor que desejamos em um ambiente de produção é o processamento em lote. Aceleradores de hardware modernos (GPUs, etc.) usados ​​para fazer inferência de aprendizado de máquina geralmente alcançam melhor eficiência computacional quando solicitações de inferência são executadas em grandes lotes.

O lote pode ser ativado fornecendo SessionBundleConfig adequado ao criar o SavedModelBundleSourceAdapter . Neste caso, definimos BatchingParameters com valores praticamente padrão. O lote pode ser ajustado definindo valores personalizados de tempo limite, batch_size, etc. Para obter detalhes, consulte BatchingParameters .

SessionBundleConfig session_bundle_config;
// Batching config
if (enable_batching) {
  BatchingParameters* batching_parameters =
      session_bundle_config.mutable_batching_parameters();
  batching_parameters->mutable_thread_pool_name()->set_value(
      "model_server_batch_threads");
}
*saved_model_bundle_source_adapter_config.mutable_legacy_config() =
    session_bundle_config;

Ao atingir o lote completo, as solicitações de inferência são mescladas internamente em uma única solicitação grande (tensor) e tensorflow::Session::Run() é invocado (de onde vem o ganho real de eficiência nas GPUs).

Servir com o gerente

Conforme mencionado acima, o TensorFlow Serving Manager foi projetado para ser um componente genérico que pode lidar com carregamento, serviço, descarregamento e transição de versão de modelos gerados por sistemas arbitrários de aprendizado de máquina. Suas APIs são construídas em torno dos seguintes conceitos-chave:

  • Servable : Servable é qualquer objeto opaco que pode ser usado para atender solicitações de clientes. O tamanho e a granularidade de um serviço são flexíveis, de modo que um único serviço pode incluir qualquer coisa, desde um único fragmento de uma tabela de pesquisa até um único modelo aprendido por máquina e uma tupla de modelos. Um serviço pode ser de qualquer tipo e interface.

  • Versão de serviço : os serviços são versionados e o TensorFlow Serving Manager pode gerenciar uma ou mais versões de um serviço. O controle de versão permite que mais de uma versão de um serviço seja carregada simultaneamente, suportando implementação e experimentação graduais.

  • Fluxo de serviço : um fluxo de serviço é a sequência de versões de um serviço, com números de versão crescentes.

  • Modelo : um modelo aprendido por máquina é representado por um ou mais serviços. Exemplos de serviços são:

    • Sessão do TensorFlow ou wrappers em torno deles, como SavedModelBundle .
    • Outros tipos de modelos aprendidos por máquina.
    • Tabelas de pesquisa de vocabulário.
    • Incorporação de tabelas de pesquisa.

    Um modelo composto pode ser representado como vários serviços independentes ou como um único serviço composto. Um serviço também pode corresponder a uma fração de um Modelo, por exemplo, com uma grande tabela de pesquisa fragmentada em muitas instâncias Manager .

Para colocar tudo isso no contexto deste tutorial:

  • Os modelos do TensorFlow são representados por um tipo de serviço – SavedModelBundle . SavedModelBundle consiste internamente em um tensorflow:Session emparelhado com alguns metadados sobre qual gráfico é carregado na sessão e como executá-lo para inferência.

  • Há um diretório do sistema de arquivos que contém um fluxo de exportações do TensorFlow, cada uma em seu próprio subdiretório cujo nome é um número de versão. O diretório externo pode ser considerado como a representação serializada do fluxo que pode ser servido para o modelo do TensorFlow que está sendo servido. Cada exportação corresponde a um serviço que pode ser carregado.

  • AspiredVersionsManager monitora o fluxo de exportação e gerencia o ciclo de vida de todos os serviços SavedModelBundle dinamicamente.

TensorflowPredictImpl::Predict então apenas:

  • Solicita SavedModelBundle do gerenciador (por meio do ServerCore).
  • Usa as generic signatures para mapear nomes de tensores lógicos em PredictRequest para nomes de tensores reais e vincular valores a tensores.
  • Executa inferência.

Teste e execute o servidor

Copie a primeira versão da exportação para a pasta monitorada:

mkdir /tmp/monitored
cp -r /tmp/mnist/1 /tmp/monitored

Então inicie o servidor:

docker run -p 8500:8500 \
  --mount type=bind,source=/tmp/monitored,target=/models/mnist \
  -t --entrypoint=tensorflow_model_server tensorflow/serving --enable_batching \
  --port=8500 --model_name=mnist --model_base_path=/models/mnist &

O servidor emitirá mensagens de log a cada segundo que dizem "Versão aspirante para serviço ...", o que significa que encontrou a exportação e está rastreando sua existência contínua.

Vamos executar o cliente com --concurrency=10 . Isso enviará solicitações simultâneas ao servidor e, assim, acionará sua lógica de lote.

tools/run_in_docker.sh python tensorflow_serving/example/mnist_client.py \
  --num_tests=1000 --server=127.0.0.1:8500 --concurrency=10

O que resulta em uma saída semelhante a:

...
Inference error rate: 13.1%

Em seguida, copiamos a segunda versão da exportação para a pasta monitorada e executamos novamente o teste:

cp -r /tmp/mnist/2 /tmp/monitored
tools/run_in_docker.sh python tensorflow_serving/example/mnist_client.py \
  --num_tests=1000 --server=127.0.0.1:8500 --concurrency=10

O que resulta em uma saída semelhante a:

...
Inference error rate: 9.5%

Isso confirma que seu servidor descobre automaticamente a nova versão e a utiliza para servir!