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:
Uma classe
Loader
que carrega, fornece acesso e descarrega uma instância deYourServable
.Um
SourceAdapter
que instancia carregadores de algum formato de dados subjacente, por exemplo, caminhos de sistema de arquivos. Como alternativa aSourceAdapter
, você poderia escrever umSource
. Entretanto, como a abordagemSourceAdapter
é 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 (se não, 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:
A lógica para carregar um hashmap de um arquivo, em
LoadHashmapFromFile()
.O uso de
SimpleLoaderSourceAdapter
para definir umSourceAdapter
que emite carregadores de hashmap baseados emLoadHashmapFromFile()
. O novoSourceAdapter
pode ser instanciado a partir de uma mensagem de protocolo de configuração do tipoHashmapSourceAdapterConfig
. 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 lambda do Criador 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.