Creare un nuovo tipo di servibile

Questo documento spiega come estendere TensorFlow Serving con un nuovo tipo di servable. Il tipo di servizio più importante è SavedModelBundle , ma può essere utile definire altri tipi di servizi, per fornire dati che vanno di pari passo con il tuo modello. Gli esempi includono: una tabella di ricerca del vocabolario, logica di trasformazione delle caratteristiche. Qualsiasi classe C++ può essere un servable, ad esempio int , std::map<string, int> o qualsiasi classe definita nel tuo binario: chiamiamola YourServable .

Definizione di un Loader e SourceAdapter per YourServable

Per consentire a TensorFlow Serving di gestire e servire YourServable , è necessario definire due cose:

  1. Una classe Loader che carica, fornisce accesso e scarica un'istanza di YourServable .

  2. Un SourceAdapter che istanzia i caricatori da alcuni formati di dati sottostanti, ad esempio percorsi del file system. In alternativa a un SourceAdapter , potresti scrivere un Source completo. Tuttavia, poiché l'approccio SourceAdapter è più comune e più modulare, qui ci concentreremo su di esso.

L'astrazione Loader è definita in core/loader.h . Richiede di definire metodi per caricare, accedere e scaricare il proprio tipo di servibile. I dati da cui viene caricato il servable possono provenire da qualsiasi luogo, ma è comune che provengano da un percorso del sistema di storage. Supponiamo che sia il caso di YourServable . Supponiamo inoltre che tu abbia già un Source<StoragePath> di cui sei soddisfatto (in caso contrario, consulta il documento Origine personalizzata ).

Oltre al tuo Loader , dovrai definire un SourceAdapter che istanzia un Loader da un determinato percorso di archiviazione. I casi d'uso più semplici possono specificare i due oggetti in modo conciso con la classe SimpleLoaderSourceAdapter (in core/simple_loader.h ). I casi d'uso avanzati possono scegliere di specificare le classi Loader e SourceAdapter separatamente utilizzando le API di livello inferiore, ad esempio se il SourceAdapter deve mantenere uno stato e/o se lo stato deve essere condiviso tra le istanze Loader .

Esiste un'implementazione di riferimento di un semplice servable hashmap che utilizza SimpleLoaderSourceAdapter in servables/hashmap/hashmap_source_adapter.cc . Potresti trovare conveniente creare una copia di HashmapSourceAdapter e quindi modificarla in base alle tue esigenze.

L'implementazione di HashmapSourceAdapter è composta da due parti:

  1. La logica per caricare una hashmap da un file, in LoadHashmapFromFile() .

  2. L'uso di SimpleLoaderSourceAdapter per definire un SourceAdapter che emette caricatori hashmap basati su LoadHashmapFromFile() . È possibile creare un'istanza del nuovo SourceAdapter da un messaggio di protocollo di configurazione di tipo HashmapSourceAdapterConfig . Attualmente, il messaggio di configurazione contiene solo il formato del file e ai fini dell'implementazione di riferimento è supportato solo un singolo formato semplice.

    Nota la chiamata a Detach() nel distruttore. Questa chiamata è necessaria per evitare gare tra lo stato di smantellamento e qualsiasi invocazione in corso del Creator lambda in altri thread. (Anche se questo semplice adattatore sorgente non ha alcuno stato, la classe base impone comunque che venga chiamato Detach().)

Organizzare il caricamento degli oggetti YourServable in un gestore

Ecco come collegare il tuo nuovo SourceAdapter per i caricatori YourServable a un'origine di base di percorsi di archiviazione e a un gestore (con cattiva gestione degli errori; il codice reale dovrebbe essere più attento):

Innanzitutto, crea un gestore:

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

Quindi, crea un adattatore sorgente YourServable e collegalo al gestore:

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

Infine, crea una sorgente di percorso semplice e collegala all'adattatore:

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

Accesso agli oggetti YourServable caricati

Ecco come ottenere un handle per un YourServable caricato e utilizzarlo:

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

Avanzato: organizzazione di più istanze gestibili per condividere lo stato

I SourceAdapter possono ospitare uno stato condiviso tra più server emessi. Per esempio:

  • Un pool di thread condiviso o un'altra risorsa utilizzata da più server.

  • Una struttura dati condivisa di sola lettura utilizzata da più serverable, per evitare il sovraccarico di tempo e spazio derivante dalla replica della struttura dati in ogni istanza gestibile.

Lo stato condiviso il cui tempo di inizializzazione e dimensione sono trascurabili (ad esempio pool di thread) può essere creato con entusiasmo dal SourceAdapter, che quindi incorpora un puntatore ad esso in ciascun caricatore servibile emesso. La creazione di stati condivisi costosi o di grandi dimensioni dovrebbe essere rinviata alla prima chiamata Loader::Load() applicabile, cioè governata dal manager. Simmetricamente, la chiamata Loader::Unload() al servibile finale utilizzando lo stato condiviso costoso/grande dovrebbe eliminarlo.