Créer un nouveau type de servable

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 :

  1. Une classe Loader qui charge, donne accès et décharge une instance de YourServable .

  2. Un SourceAdapter qui instancie les chargeurs à partir d'un format de données sous-jacent, par exemple les chemins du système de fichiers. Comme alternative à un SourceAdapter , vous pouvez écrire un Source complet. Cependant, comme l’approche SourceAdapter 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 :

  1. La logique pour charger un hashmap à partir d'un fichier, dans LoadHashmapFromFile() .

  2. L'utilisation de SimpleLoaderSourceAdapter pour définir un SourceAdapter qui émet des chargeurs de hashmap basés sur LoadHashmapFromFile() . Le nouveau SourceAdapter peut être instancié à partir d'un message de protocole de configuration de type HashmapSourceAdapterConfig . 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.