Criando um novo tipo de serviço

Este documento explica como estender o TensorFlow Serving com um novo tipo de serviço. O tipo de serviço mais proeminente é SavedModelBundle , mas pode ser útil para definir outros tipos de serviços, para fornecer dados que acompanham seu modelo. Os exemplos incluem: uma tabela de pesquisa de vocabulário, lógica de transformação de recursos. Qualquer classe C++ pode ser servilável, por exemplo, int , std::map<string, int> ou qualquer classe definida em seu binário - vamos chamá-la de YourServable .

Definindo um Loader e SourceAdapter para YourServable

Para permitir que o TensorFlow Serving gerencie e forneça YourServable , você precisa definir duas coisas:

  1. Uma classe Loader que carrega, fornece acesso e descarrega uma instância de YourServable .

  2. Um SourceAdapter que instancia carregadores de algum formato de dados subjacente, por exemplo, caminhos de sistema de arquivos. Como alternativa a SourceAdapter , você poderia escrever um Source . Entretanto, como a abordagem SourceAdapter é mais comum e mais modular, focamos nela aqui.

A abstração Loader é definida em core/loader.h . Requer que você defina métodos para carregar, acessar e descarregar seu tipo de serviço. Os dados dos quais o serviço é carregado podem vir de qualquer lugar, mas é comum que venham de um caminho do sistema de armazenamento. Suponhamos que esse seja o caso de YourServable . Suponhamos ainda que você já tenha um Source<StoragePath> com o qual esteja satisfeito (caso contrário, consulte o documento Fonte personalizada ).

Além do seu Loader , você precisará definir um SourceAdapter que instancie um Loader a partir de um determinado caminho de armazenamento. A maioria dos casos de uso simples pode especificar os dois objetos de forma concisa com a classe SimpleLoaderSourceAdapter (em core/simple_loader.h ). Casos de uso avançados podem optar por especificar as classes Loader e SourceAdapter separadamente usando APIs de nível inferior, por exemplo, se o SourceAdapter precisar reter algum estado e/ou se o estado precisar ser compartilhado entre instâncias Loader .

Há uma implementação de referência de um hashmap simples que usa SimpleLoaderSourceAdapter em servables/hashmap/hashmap_source_adapter.cc . Você pode achar conveniente fazer uma cópia do HashmapSourceAdapter e modificá-lo para atender às suas necessidades.

A implementação do HashmapSourceAdapter tem duas partes:

  1. A lógica para carregar um hashmap de um arquivo, em LoadHashmapFromFile() .

  2. O uso de SimpleLoaderSourceAdapter para definir um SourceAdapter que emite carregadores de hashmap baseados em LoadHashmapFromFile() . O novo SourceAdapter pode ser instanciado a partir de uma mensagem de protocolo de configuração do tipo HashmapSourceAdapterConfig . Atualmente, a mensagem de configuração contém apenas o formato do arquivo e, para fins de implementação de referência, apenas um único formato simples é suportado.

    Observe a chamada para Detach() no destruidor. Esta chamada é necessária para evitar corridas entre o estado de desmontagem e quaisquer invocações contínuas do criador lambda em outros threads. (Mesmo que este adaptador de origem simples não tenha nenhum estado, a classe base ainda assim impõe que Detach() seja chamado.)

Organizando para que objetos YourServable sejam carregados em um gerenciador

Veja como conectar seu novo carregador SourceAdapter para YourServable a uma fonte básica de caminhos de armazenamento e a um gerenciador (com tratamento incorreto de erros; o código real deve ser mais cuidadoso):

Primeiro, crie um gerente:

std::unique_ptr<AspiredVersionsManager> manager = ...;

Em seguida, crie um adaptador de origem YourServable e conecte-o ao gerenciador:

auto your_adapter = new YourServableSourceAdapter(...);
ConnectSourceToTarget(your_adapter, manager.get());

Por último, crie uma fonte de caminho simples e conecte-a ao seu adaptador:

std::unique_ptr<FileSystemStoragePathSource> path_source;
// Here are some FileSystemStoragePathSource config settings that ought to get
// it working, but for details please see its documentation.
FileSystemStoragePathSourceConfig config;
// We just have a single servable stream. Call it "default".
config.set_servable_name("default");
config.set_base_path(FLAGS::base_path /* base path for our servable files */);
config.set_file_system_poll_wait_seconds(1);
TF_CHECK_OK(FileSystemStoragePathSource::Create(config, &path_source));
ConnectSourceToTarget(path_source.get(), your_adapter.get());

Acessando objetos YourServable carregados

Veja como obter um identificador para um YourServable carregado e usá-lo:

auto handle_request = serving::ServableRequest::Latest("default");
ServableHandle<YourServable*> servable;
Status status = manager->GetServableHandle(handle_request, &servable);
if (!status.ok()) {
  LOG(INFO) << "Zero versions of 'default' servable have been loaded so far";
  return;
}
// Use the servable.
(*servable)->SomeYourServableMethod();

Avançado: Organizando múltiplas instâncias utilizáveis ​​para compartilhar estado

SourceAdapters podem hospedar o estado que é compartilhado entre vários serviços emitidos. Por exemplo:

  • Um conjunto de encadeamentos compartilhado ou outro recurso usado por vários serviços.

  • Uma estrutura de dados somente leitura compartilhada que vários serviços usam, para evitar a sobrecarga de tempo e espaço de replicação da estrutura de dados em cada instância de serviço.

O estado compartilhado cujo tempo e tamanho de inicialização são insignificantes (por exemplo, pools de threads) pode ser criado avidamente pelo SourceAdapter, que então incorpora um ponteiro para ele em cada carregador utilizável emitido. A criação de um estado compartilhado caro ou grande deve ser adiada para a primeira chamada Loader::Load() aplicável, ou seja, governada pelo gerente. Simetricamente, a chamada Loader::Unload() para o serviço final usando o estado compartilhado caro/grande deve destruí-lo.