Tworzenie nowego rodzaju serwowalnego

W tym dokumencie wyjaśniono, jak rozszerzyć obsługę TensorFlow o nowy rodzaj obiektu udostępnianego. Najbardziej znanym typem serwowalnym jest SavedModelBundle , ale przydatne może być zdefiniowanie innych rodzajów obiektów serwowalnych, aby udostępniać dane pasujące do Twojego modelu. Przykłady obejmują: tabelę wyszukiwania słownictwa, logikę transformacji funkcji. Dowolna klasa C++ może być obsługiwana, np. int , std::map<string, int> lub dowolna klasa zdefiniowana w pliku binarnym — nazwijmy ją YourServable .

Definiowanie Loader i SourceAdapter dla YourServable

Aby umożliwić TensorFlow Serving zarządzanie i obsługę YourServable , musisz zdefiniować dwie rzeczy:

  1. Klasa Loader , która ładuje, zapewnia dostęp i rozładowuje instancję YourServable .

  2. SourceAdapter , który tworzy instancje modułów ładujących z pewnego podstawowego formatu danych, np. ścieżek systemu plików. Alternatywą dla SourceAdapter jest możliwość napisania kompletnego Source . Ponieważ jednak podejście SourceAdapter jest bardziej powszechne i bardziej modułowe, skupiamy się na nim tutaj.

Abstrakcja Loader jest zdefiniowana w core/loader.h . Wymaga zdefiniowania metod ładowania, uzyskiwania dostępu i rozładowywania typu serwowalnego. Dane, z których ładowany jest obiekt servable, mogą pochodzić z dowolnego miejsca, ale często pochodzą ze ścieżki systemu pamięci masowej. Załóżmy, że tak jest w przypadku YourServable . Załóżmy dalej, że masz już Source<StoragePath> , z którego jesteś zadowolony (jeśli nie, zobacz dokument Niestandardowe źródło ).

Oprócz Loader będziesz musiał zdefiniować SourceAdapter , który utworzy instancję Loader z danej ścieżki przechowywania. W większości prostych przypadków użycia można zwięźle określić dwa obiekty za pomocą klasy SimpleLoaderSourceAdapter (w core/simple_loader.h ). Zaawansowane przypadki użycia mogą zdecydować się na oddzielne określenie klas Loader i SourceAdapter przy użyciu interfejsów API niższego poziomu, np. jeśli SourceAdapter musi zachować jakiś stan i/lub jeśli stan musi być współdzielony pomiędzy instancjami Loader .

Istnieje referencyjna implementacja prostego serwable hashmap, która używa SimpleLoaderSourceAdapter w servables/hashmap/hashmap_source_adapter.cc . Może się okazać, że wygodnie będzie utworzyć kopię HashmapSourceAdapter , a następnie zmodyfikować ją tak, aby odpowiadała Twoim potrzebom.

Implementacja HashmapSourceAdapter składa się z dwóch części:

  1. Logika ładowania mapy mieszającej z pliku w LoadHashmapFromFile() .

  2. Użycie SimpleLoaderSourceAdapter do zdefiniowania SourceAdapter , który emituje moduły ładujące hashmap w oparciu o LoadHashmapFromFile() . Nową SourceAdapter można utworzyć z komunikatu protokołu konfiguracji typu HashmapSourceAdapterConfig . Obecnie komunikat konfiguracyjny zawiera tylko format pliku, a na potrzeby implementacji referencyjnej obsługiwany jest tylko jeden prosty format.

    Zwróć uwagę na wywołanie Detach() w destruktorze. To wywołanie jest wymagane, aby uniknąć wyścigów między zniszczeniem stanu a wszelkimi ciągłymi wywołaniami lambdy Stwórcy w innych wątkach. (Chociaż ten prosty adapter źródłowy nie ma żadnego stanu, klasa bazowa mimo wszystko wymusza wywołanie funkcji Detach().)

Organizowanie ładowania obiektów YourServable do menedżera

Oto jak podłączyć nowe moduły ładujące SourceAdapter for YourServable do podstawowego źródła ścieżek pamięci i menedżera (ze złą obsługą błędów; prawdziwy kod powinien być ostrożniejszy):

Najpierw utwórz menedżera:

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

Następnie utwórz adapter źródłowy YourServable i podłącz go do menedżera:

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

Na koniec utwórz proste źródło ścieżki i podłącz je do adaptera:

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

Dostęp do załadowanych obiektów YourServable

Oto jak uzyskać uchwyt do załadowanego YourServable i użyć go:

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

Zaawansowane: organizowanie wielu obsługiwanych instancji w celu współdzielenia stanu

SourceAdapters może przechowywać stan, który jest współdzielony pomiędzy wieloma emitowanymi obiektami serwable. Na przykład:

  • Wspólna pula wątków lub inny zasób, z którego korzysta wiele obiektów serwowalnych.

  • Współdzielona struktura danych tylko do odczytu, z której korzysta wiele obiektów serwowalnych, aby uniknąć narzutu czasowego i przestrzennego związanego z replikacją struktury danych w każdej udostępnianej instancji.

Stan współdzielony, którego czas inicjalizacji i rozmiar są znikome (np. pule wątków), może zostać chętnie utworzony przez SourceAdapter, który następnie osadza wskaźnik do niego w każdym wyemitowanym obsługiwanym programie ładującym. Utworzenie kosztownego lub dużego stanu współdzielonego powinno zostać odroczone do pierwszego odpowiedniego wywołania Loader::Load(), tj. zarządzanego przez menedżera. Symetrycznie, wywołanie Loader::Unload() do końcowego obiektu obsłużonego przy użyciu drogiego/dużego stanu współdzielonego powinno go zniszczyć.