Ce document explique comment étendre TensorFlow Serving avec un nouveau type de servable. Le type de servable le plus important est SavedModelBundle
, mais il peut être utile de définir d'autres types de servables, pour servir les données qui accompagnent votre modèle. Les exemples incluent : une table de recherche de vocabulaire, une logique de transformation de fonctionnalités. N'importe quelle classe C++ peut être un servable, par exemple int
, std::map<string, int>
ou n'importe quelle classe définie dans votre binaire -- appelons-la YourServable
.
Définition d'un Loader
et SourceAdapter
pour YourServable
Pour permettre à TensorFlow Serving de gérer et de servir YourServable
, vous devez définir deux éléments :
Une classe
Loader
qui charge, donne accès et décharge une instance deYourServable
.Un
SourceAdapter
qui instancie les chargeurs à partir d'un certain format de données sous-jacent, par exemple les chemins du système de fichiers. Comme alternative à unSourceAdapter
, vous pouvez écrire unSource
complet. Cependant, comme l’approcheSourceAdapter
est plus courante et plus modulaire, nous nous concentrons ici sur elle.
L'abstraction Loader
est définie dans core/loader.h
. Cela vous oblige à définir des méthodes de chargement, d'accès et de déchargement de votre type de servable. Les données à partir desquelles le servable est chargé peuvent provenir de n'importe où, mais il est courant qu'elles proviennent d'un chemin du système de stockage. Supposons que ce soit le cas pour YourServable
. Supposons en outre que vous disposez déjà d'un Source<StoragePath>
dont vous êtes satisfait (sinon, consultez le document Source personnalisée ).
En plus de votre Loader
, vous devrez définir un SourceAdapter
qui instancie un Loader
à partir d'un chemin de stockage donné. La plupart des cas d'utilisation simples peuvent spécifier les deux objets de manière concise avec la classe SimpleLoaderSourceAdapter
(dans core/simple_loader.h
). Les cas d'utilisation avancés peuvent choisir de spécifier les classes Loader
et SourceAdapter
séparément à l'aide des API de niveau inférieur, par exemple si le SourceAdapter
doit conserver un certain état et/ou si l'état doit être partagé entre les instances Loader
.
Il existe une implémentation de référence d'un simple servable hashmap qui utilise SimpleLoaderSourceAdapter
dans servables/hashmap/hashmap_source_adapter.cc
. Vous trouverez peut-être pratique de faire une copie de HashmapSourceAdapter
, puis de la modifier en fonction de vos besoins.
L'implémentation de HashmapSourceAdapter
comporte deux parties :
La logique pour charger un hashmap à partir d'un fichier, dans
LoadHashmapFromFile()
.L'utilisation de
SimpleLoaderSourceAdapter
pour définir unSourceAdapter
qui émet des chargeurs de hashmap basés surLoadHashmapFromFile()
. Le nouveauSourceAdapter
peut être instancié à partir d'un message de protocole de configuration de typeHashmapSourceAdapterConfig
. Actuellement, le message de configuration contient uniquement le format de fichier et, pour les besoins de l'implémentation de référence, un seul format simple est pris en charge.Notez l'appel à
Detach()
dans le destructeur. Cet appel est nécessaire pour éviter les courses entre l'état de suppression et toute invocation en cours du Créateur lambda dans d'autres threads. (Même si cet adaptateur source simple n'a aucun état, la classe de base impose néanmoins que Detach() soit appelé.)
Organiser le chargement des objets YourServable
dans un gestionnaire
Voici comment connecter vos nouveaux chargeurs SourceAdapter
pour YourServable
à une source de base de chemins de stockage et à un gestionnaire (avec une mauvaise gestion des erreurs ; le vrai code devrait être plus prudent) :
Tout d'abord, créez un gestionnaire :
std::unique_ptr<AspiredVersionsManager> manager = ...;
Ensuite, créez un adaptateur source YourServable
et branchez-le au gestionnaire :
auto your_adapter = new YourServableSourceAdapter(...);
ConnectSourceToTarget(your_adapter, manager.get());
Enfin, créez une source de chemin simple et branchez-la sur votre adaptateur :
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());
Accéder aux objets YourServable
chargés
Voici comment obtenir un handle vers un YourServable
chargé et l'utiliser :
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();
Avancé : organisation de plusieurs instances utilisables pour partager l'état
Les SourceAdapters peuvent héberger un état partagé entre plusieurs servables émis. Par exemple:
Un pool de threads partagé ou une autre ressource utilisée par plusieurs servables.
Une structure de données partagée en lecture seule que plusieurs servables utilisent, pour éviter la surcharge de temps et d'espace liée à la réplication de la structure de données dans chaque instance servable.
Un état partagé dont le temps d'initialisation et la taille sont négligeables (par exemple, des pools de threads) peut être créé avec impatience par le SourceAdapter, qui intègre ensuite un pointeur vers lui dans chaque chargeur servable émis. La création d'un état partagé coûteux ou volumineux doit être reportée au premier appel Loader::Load() applicable, c'est-à-dire régi par le gestionnaire. Symétriquement, l'appel Loader::Unload() au servable final utilisant l'état partagé coûteux/large devrait le détruire.