Creando un nuevo tipo de servible

Este documento explica cómo ampliar TensorFlow Serving con un nuevo tipo de servable. El tipo de servable más destacado es SavedModelBundle , pero puede ser útil para definir otros tipos de servables, para entregar datos que vayan junto con su modelo. Los ejemplos incluyen: una tabla de búsqueda de vocabulario, lógica de transformación de características. Cualquier clase de C++ puede ser servible, por ejemplo, int , std::map<string, int> o cualquier clase definida en su binario; llamémosla YourServable .

Definición de un Loader y SourceAdapter para YourServable

Para permitir que TensorFlow Serving administre y proporcione YourServable , debe definir dos cosas:

  1. Una clase Loader que carga, proporciona acceso y descarga una instancia de YourServable .

  2. Un SourceAdapter que crea instancias de cargadores a partir de algún formato de datos subyacente, por ejemplo, rutas del sistema de archivos. Como alternativa a un SourceAdapter , puedes escribir un Source completo. Sin embargo, dado que el enfoque SourceAdapter es más común y más modular, nos centraremos en él aquí.

La abstracción Loader se define en core/loader.h . Requiere que defina métodos para cargar, acceder y descargar su tipo de servable. Los datos desde los que se carga el servable pueden provenir de cualquier lugar, pero es común que provengan de una ruta del sistema de almacenamiento. Supongamos que ese es el caso de YourServable . Supongamos además que ya tiene una Source<StoragePath> con la que está satisfecho (si no, consulte el documento Fuente personalizada ).

Además de su Loader , deberá definir un SourceAdapter que cree una instancia de un Loader desde una ruta de almacenamiento determinada. La mayoría de los casos de uso simples pueden especificar los dos objetos de manera concisa con la clase SimpleLoaderSourceAdapter (en core/simple_loader.h ). Los casos de uso avanzados pueden optar por especificar las clases Loader y SourceAdapter por separado utilizando las API de nivel inferior, por ejemplo, si SourceAdapter necesita conservar algún estado y/o si el estado debe compartirse entre instancias Loader .

Hay una implementación de referencia de un servable hashmap simple que usa SimpleLoaderSourceAdapter en servables/hashmap/hashmap_source_adapter.cc . Puede que le resulte conveniente hacer una copia de HashmapSourceAdapter y luego modificarla para adaptarla a sus necesidades.

La implementación de HashmapSourceAdapter consta de dos partes:

  1. La lógica para cargar un mapa hash desde un archivo, en LoadHashmapFromFile() .

  2. El uso de SimpleLoaderSourceAdapter para definir un SourceAdapter que emite cargadores de mapas hash basados ​​en LoadHashmapFromFile() . Se puede crear una instancia del nuevo SourceAdapter a partir de un mensaje de protocolo de configuración de tipo HashmapSourceAdapterConfig . Actualmente, el mensaje de configuración contiene solo el formato de archivo y, a efectos de la implementación de referencia, solo se admite un formato simple.

    Tenga en cuenta la llamada a Detach() en el destructor. Esta llamada es necesaria para evitar carreras entre la destrucción del estado y cualquier invocación en curso del Creador lambda en otros hilos. (Aunque este adaptador de fuente simple no tiene ningún estado, la clase base impone que se llame a Detach()).

Organizar la carga de objetos YourServable en un administrador

A continuación se explica cómo conectar su nuevo SourceAdapter para cargadores YourServable a una fuente básica de rutas de almacenamiento y a un administrador (con un mal manejo de errores; el código real debería tener más cuidado):

Primero, crea un administrador:

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

Luego, cree un adaptador de fuente YourServable y conéctelo al administrador:

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

Por último, cree una fuente de ruta simple y conéctela a su 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());

Accediendo a objetos YourServable cargados

Aquí se explica cómo obtener un identificador de un YourServable cargado y usarlo:

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();

Avanzado: organización de múltiples instancias servibles para compartir el estado

SourceAdapters puede albergar un estado compartido entre varios servables emitidos. Por ejemplo:

  • Un grupo de subprocesos compartido u otro recurso que utilizan varios servidores.

  • Una estructura de datos compartida de solo lectura que utilizan varios servidores para evitar la sobrecarga de tiempo y espacio de replicar la estructura de datos en cada instancia de servicio.

El estado compartido cuyo tiempo y tamaño de inicialización es insignificante (por ejemplo, grupos de subprocesos) puede ser creado con entusiasmo por SourceAdapter, que luego incorpora un puntero a él en cada cargador de servicio emitido. La creación de un estado compartido costoso o grande debe diferirse hasta la primera llamada Loader::Load() aplicable, es decir, gobernada por el administrador. Simétricamente, la llamada Loader::Unload() al servidor final usando el estado compartido caro/grande debería derribarlo.