Wdrażanie niestandardowego przedstawiciela

Delegat TensorFlow Lite umożliwia uruchamianie modeli (części lub całości) z innym wykonawcą. Mechanizm wnioskowania może korzystać z różnych akceleratorów działających na urządzeniu, takich jak GPU czy Edge TPU (Tensor Processor Unit). Dzięki temu deweloperzy mogą korzystać z elastycznej metody niepołączonej z domyślnym TFLite, co przyspiesza wnioskowanie.

Poniższy schemat przedstawia podsumowanie przedstawicieli. Więcej szczegółów znajdziesz w sekcjach poniżej.

Przedstawiciele TFLite

Kiedy należy utworzyć niestandardowego przedstawiciela?

TensorFlow Lite ma szeroką gamę przedstawicieli akceleratorów docelowych, takich jak GPU, DSP i EdgeTPU.

Tworzenie własnego przedstawiciela jest przydatne w tych sytuacjach:

  • Chcesz zintegrować nowy mechanizm wnioskowania systemów uczących się, który nie jest obsługiwany przez żadnego obecnego przedstawiciela.
  • Masz niestandardowy akcelerator sprzętowy, który poprawia środowisko wykonawcze w znanych sytuacjach.
  • Opracowujesz optymalizacje procesora (np. łączenie operatorów), które mogą przyspieszyć niektóre modele.

Jak działają przedstawiciele?

Rozważmy prosty wykres modelu, taki jak poniżej, oraz delegata „MyDelegate”, który ma szybszą implementację dla operacji typu „Konw2D” i „średnia”.

Pierwotny wykres

Po zastosowaniu tego obiektu „MyDelegate” (MyDelegate) oryginalny wykres TensorFlow Lite zostanie zaktualizowany w taki sposób:

Wykres z przekazanym dostępem

Powyższy wykres jest generowany, gdy TensorFlow Lite dzieli pierwotny wykres, stosując 2 reguły:

  • Określone operacje, które mogą być obsługiwane przez osobę, której przekazano dostęp, są umieszczane w partycji przy jednoczesnym spełnianiu zależności między operacjami, które bazują na pierwotnym procesie przetwarzania.
  • Każda partycja do przekazania ma tylko węzły wejściowe i wyjściowe, których nie obsługuje osoba z przekazanym dostępem.

Każda partycja obsługiwana przez delegata jest zastępowana węzłem delegata (może być też wywołana na oryginalnym wykresie jako jądro delegata), który ocenia partycję w wywołaniu jej wywołania.

W zależności od modelu końcowy wykres może zawierać 1 węzeł lub więcej. Ten ostatni oznacza, że niektóre operacje nie są obsługiwane przez przedstawiciela. Ogólnie rzecz biorąc, osoba, której przekazano dostęp, nie powinna obsługiwać wielu partycji, ponieważ za każdym razem, gdy przechodzisz z wykresu z przekazanym dostępem do wykresu głównego, wiąże się to z problemem z przekazywaniem wyników z podgrafu delegowanego do wykresu głównego w wyniku kopii pamięci (np. z GPU na CPU). Może to równoważyć wzrost wydajności, zwłaszcza gdy istnieje duża liczba kopii pamięci.

Wdrażanie własnego niestandardowego przedstawiciela

Preferowaną metodą dodawania delegata jest użycie interfejsu SimpleDelegate API.

Aby utworzyć nowego przedstawiciela, musisz wdrożyć 2 interfejsy i udostępnić własną implementację metod interfejsu.

1–SimpleDelegateInterface

Ta klasa reprezentuje możliwości delegata, obsługiwane operacje oraz klasę fabryczną służącą do utworzenia jądra, który zawiera przekazany wykres. Więcej informacji znajdziesz w interfejsie zdefiniowanego w tym pliku nagłówka C++. W komentarzach w kodzie znajdziesz szczegółowe informacje o poszczególnych interfejsach API.

2 – SimpleDelegateKernelInterface

Ta klasa zawiera logikę inicjowania, przygotowywania i uruchamiania delegowanej partycji.

Zawiera: (zobacz definicję)

  • Init(...): element, który zostanie wywołany raz w celu wykonania dowolnej jednorazowej inicjalizacji.
  • Przygotowanie(...): wywoływanie dla każdej instancji tego węzła. Dzieje się tak, jeśli masz wiele partycji delegowanych. W tym miejscu zazwyczaj najlepiej jest wykonywać przydziały pamięci, ponieważ będzie ono wywoływane za każdym razem, gdy zmienia się rozmiar tensorów.
  • Wywołaj(...): element wywoływany w celu wnioskowania.

Przykład

W tym przykładzie utworzysz bardzo prosty delegat, który może obsługiwać tylko 2 typy operacji (ADD) i (SUB) z tensorami float32.

// MyDelegate implements the interface of SimpleDelegateInterface.
// This holds the Delegate capabilities.
class MyDelegate : public SimpleDelegateInterface {
 public:
  bool IsNodeSupportedByDelegate(const TfLiteRegistration* registration,
                                 const TfLiteNode* node,
                                 TfLiteContext* context) const override {
    // Only supports Add and Sub ops.
    if (kTfLiteBuiltinAdd != registration->builtin_code &&
        kTfLiteBuiltinSub != registration->builtin_code)
      return false;
    // This delegate only supports float32 types.
    for (int i = 0; i < node->inputs->size; ++i) {
      auto& tensor = context->tensors[node->inputs->data[i]];
      if (tensor.type != kTfLiteFloat32) return false;
    }
    return true;
  }

  TfLiteStatus Initialize(TfLiteContext* context) override { return kTfLiteOk; }

  const char* Name() const override {
    static constexpr char kName[] = "MyDelegate";
    return kName;
  }

  std::unique_ptr<SimpleDelegateKernelInterface> CreateDelegateKernelInterface()
      override {
    return std::make_unique<MyDelegateKernel>();
  }
};

Następnie utwórz własne jądro delegata przez dziedziczenie z instancji SimpleDelegateKernelInterface

// My delegate kernel.
class MyDelegateKernel : public SimpleDelegateKernelInterface {
 public:
  TfLiteStatus Init(TfLiteContext* context,
                    const TfLiteDelegateParams* params) override {
    // Save index to all nodes which are part of this delegate.
    inputs_.resize(params->nodes_to_replace->size);
    outputs_.resize(params->nodes_to_replace->size);
    builtin_code_.resize(params->nodes_to_replace->size);
    for (int i = 0; i < params->nodes_to_replace->size; ++i) {
      const int node_index = params->nodes_to_replace->data[i];
      // Get this node information.
      TfLiteNode* delegated_node = nullptr;
      TfLiteRegistration* delegated_node_registration = nullptr;
      TF_LITE_ENSURE_EQ(
          context,
          context->GetNodeAndRegistration(context, node_index, &delegated_node,
                                          &delegated_node_registration),
          kTfLiteOk);
      inputs_[i].push_back(delegated_node->inputs->data[0]);
      inputs_[i].push_back(delegated_node->inputs->data[1]);
      outputs_[i].push_back(delegated_node->outputs->data[0]);
      builtin_code_[i] = delegated_node_registration->builtin_code;
    }
    return kTfLiteOk;
  }

  TfLiteStatus Prepare(TfLiteContext* context, TfLiteNode* node) override {
    return kTfLiteOk;
  }

  TfLiteStatus Eval(TfLiteContext* context, TfLiteNode* node) override {
    // Evaluate the delegated graph.
    // Here we loop over all the delegated nodes.
    // We know that all the nodes are either ADD or SUB operations and the
    // number of nodes equals ''inputs_.size()'' and inputs[i] is a list of
    // tensor indices for inputs to node ''i'', while outputs_[i] is the list of
    // outputs for node
    // ''i''. Note, that it is intentional we have simple implementation as this
    // is for demonstration.

    for (int i = 0; i < inputs_.size(); ++i) {
      // Get the node input tensors.
      // Add/Sub operation accepts 2 inputs.
      auto& input_tensor_1 = context->tensors[inputs_[i][0]];
      auto& input_tensor_2 = context->tensors[inputs_[i][1]];
      auto& output_tensor = context->tensors[outputs_[i][0]];
      TF_LITE_ENSURE_EQ(
          context,
          ComputeResult(context, builtin_code_[i], &input_tensor_1,
                        &input_tensor_2, &output_tensor),
          kTfLiteOk);
    }
    return kTfLiteOk;
  }

 private:
  // Computes the result of addition of 'input_tensor_1' and 'input_tensor_2'
  // and store the result in 'output_tensor'.
  TfLiteStatus ComputeResult(TfLiteContext* context, int builtin_code,
                             const TfLiteTensor* input_tensor_1,
                             const TfLiteTensor* input_tensor_2,
                             TfLiteTensor* output_tensor) {
    if (NumElements(input_tensor_1) != NumElements(input_tensor_2) ||
        NumElements(input_tensor_1) != NumElements(output_tensor)) {
      return kTfLiteDelegateError;
    }
    // This code assumes no activation, and no broadcasting needed (both inputs
    // have the same size).
    auto* input_1 = GetTensorData<float>(input_tensor_1);
    auto* input_2 = GetTensorData<float>(input_tensor_2);
    auto* output = GetTensorData<float>(output_tensor);
    for (int i = 0; i < NumElements(input_tensor_1); ++i) {
      if (builtin_code == kTfLiteBuiltinAdd)
        output[i] = input_1[i] + input_2[i];
      else
        output[i] = input_1[i] - input_2[i];
    }
    return kTfLiteOk;
  }

  // Holds the indices of the input/output tensors.
  // inputs_[i] is list of all input tensors to node at index 'i'.
  // outputs_[i] is list of all output tensors to node at index 'i'.
  std::vector<std::vector<int>> inputs_, outputs_;
  // Holds the builtin code of the ops.
  // builtin_code_[i] is the type of node at index 'i'
  std::vector<int> builtin_code_;
};


Porównywanie i ocenianie nowego przedstawiciela

TFLite udostępnia zestaw narzędzi, które można szybko przetestować za pomocą modelu TFLite.

  • Narzędzie do analizy porównawczej modeli: narzędzie wykorzystuje model TFLite, generuje losowe dane wejściowe, a następnie wielokrotnie uruchamia model przez określoną liczbę uruchomień. Na końcu wydrukuje zagregowane statystyki czasu oczekiwania.
  • Narzędzie wnioskowania: w przypadku danego modelu narzędzie generuje losowe dane Gaussa i przekazuje je przez 2 różne interpretery TFLite, z których jeden działa w jednym z jednym wątku procesora, a drugi przy użyciu specyfikacji zdefiniowanej przez użytkownika. Mierzy bezwzględną różnicę między tensorami wyjściowymi poszczególnych interpreterów. Narzędzie to może się też przydać do debugowania problemów z dokładnością.
  • Istnieją też narzędzia do oceny konkretnych zadań, służące do klasyfikacji obrazów i wykrywania obiektów. Narzędzia te znajdziesz tutaj

Dodatkowo TFLite ma duży zestaw testów jądra i jednostki operacyjnej, które można wykorzystać ponownie do przetestowania nowego przedstawiciela o większym zasięgu i zapewnienia, że zwykła ścieżka wykonywania TFLite nie zostanie przerwana.

Aby ponownie wykorzystać testy i narzędzia TFLite dla nowego przedstawiciela, wybierz jedną z dwóch opcji:

Wybór najlepszego podejścia

Oba sposoby wymagają wprowadzenia kilku zmian, które opisaliśmy poniżej. Pierwsze podejście wiąże się jednak ze statyczną umocnieniem delegata i wymaga przebudowania narzędzi do testowania, testów porównawczych i oceny. W przeciwieństwie do tego drugi zasób staje się biblioteką współdzieloną, przez co użytkownik musi ujawnić metody tworzenia i usuwania z zasobów wspólnych.

Dzięki temu mechanizm przekazywania dostępu zewnętrznego będzie działać z gotowymi plikami binarnymi narzędzi Tensorflow Lite z oferty TFLite. Jest to jednak mniej jednoznaczne i może być trudniejsze do skonfigurowania w automatycznych testach integracji. Aby uniknąć wątpliwości, stosuj podejście rejestratora z przekazanym dostępem.

Opcja 1. Skorzystaj z delegata rejestratora

Rejestrator przekazany prowadzi listę dostawców przedstawicieli. Każdy z nich ułatwia tworzenie przedstawicieli TFLite na podstawie flag wiersza poleceń, dzięki czemu jest łatwy w obsłudze narzędzi. Aby dodać nowego przedstawiciela do wszystkich wymienionych powyżej narzędzi Tensorflow Lite, trzeba najpierw utworzyć nowego dostawcę przekazywania dostępu, a potem wprowadzić tylko kilka zmian w regułach BUILD. Pełny przykład procesu integracji znajdziesz poniżej (a kod znajdziesz tutaj).

Zakładamy, że masz przedstawiciela, który implementuje interfejsy SimpleDelegate API, oraz zewnętrzne interfejsy API „C” służące do tworzenia lub usuwania tego delegata „fikcyjnego”, jak pokazano poniżej:

// Returns default options for DummyDelegate.
DummyDelegateOptions TfLiteDummyDelegateOptionsDefault();

// Creates a new delegate instance that need to be destroyed with
// `TfLiteDummyDelegateDelete` when delegate is no longer used by TFLite.
// When `options` is set to `nullptr`, the above default values are used:
TfLiteDelegate* TfLiteDummyDelegateCreate(const DummyDelegateOptions* options);

// Destroys a delegate created with `TfLiteDummyDelegateCreate` call.
void TfLiteDummyDelegateDelete(TfLiteDelegate* delegate);

Aby zintegrować parametr „DummyDelegate” z analizą porównawczą i narzędziem do wnioskowania, zdefiniuj parametr DelegateProvider poniżej:

class DummyDelegateProvider : public DelegateProvider {
 public:
  DummyDelegateProvider() {
    default_params_.AddParam("use_dummy_delegate",
                             ToolParam::Create<bool>(false));
  }

  std::vector<Flag> CreateFlags(ToolParams* params) const final;

  void LogParams(const ToolParams& params) const final;

  TfLiteDelegatePtr CreateTfLiteDelegate(const ToolParams& params) const final;

  std::string GetName() const final { return "DummyDelegate"; }
};
REGISTER_DELEGATE_PROVIDER(DummyDelegateProvider);

std::vector<Flag> DummyDelegateProvider::CreateFlags(ToolParams* params) const {
  std::vector<Flag> flags = {CreateFlag<bool>("use_dummy_delegate", params,
                                              "use the dummy delegate.")};
  return flags;
}

void DummyDelegateProvider::LogParams(const ToolParams& params) const {
  TFLITE_LOG(INFO) << "Use dummy test delegate : ["
                   << params.Get<bool>("use_dummy_delegate") << "]";
}

TfLiteDelegatePtr DummyDelegateProvider::CreateTfLiteDelegate(
    const ToolParams& params) const {
  if (params.Get<bool>("use_dummy_delegate")) {
    auto default_options = TfLiteDummyDelegateOptionsDefault();
    return TfLiteDummyDelegateCreateUnique(&default_options);
  }
  return TfLiteDelegatePtr(nullptr, [](TfLiteDelegate*) {});
}

Definicje reguł BUILD są ważne, ponieważ trzeba upewnić się, że biblioteka jest zawsze połączona i nie jest pomijana przez optymalizatora.

#### The following are for using the dummy test delegate in TFLite tooling ####
cc_library(
    name = "dummy_delegate_provider",
    srcs = ["dummy_delegate_provider.cc"],
    copts = tflite_copts(),
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/tools/delegates:delegate_provider_hdr",
    ],
    alwayslink = 1, # This is required so the optimizer doesn't optimize the library away.
)

Teraz dodaj te 2 reguły otoki w pliku BUILD, aby utworzyć wersję narzędzia do analizy porównawczej i narzędzia do wnioskowania oraz innych narzędzi do oceny, które można uruchamiać z użyciem własnego przedstawiciela.

cc_binary(
    name = "benchmark_model_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/benchmark:benchmark_model_main",
    ],
)

cc_binary(
    name = "inference_diff_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/inference_diff:run_eval_lib",
    ],
)

cc_binary(
    name = "imagenet_classification_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/imagenet_image_classification:run_eval_lib",
    ],
)

cc_binary(
    name = "coco_object_detection_eval_plus_dummy_delegate",
    copts = tflite_copts(),
    linkopts = task_linkopts(),
    deps = [
        ":dummy_delegate_provider",
        "//tensorflow/lite/tools/evaluation/tasks:task_executor_main",
        "//tensorflow/lite/tools/evaluation/tasks/coco_object_detection:run_eval_lib",
    ],
)

Możesz też podłączyć tego dostawcę do testów jądra TFLite zgodnie z opisem w tym artykule.

Opcja 2. Wykorzystaj przedstawiciela zewnętrznego

W ramach tego rozwiązania najpierw utworzysz adapter zewnętrznego przedstawiciela w pliku external_delegate_adaptor.cc, jak pokazano poniżej. Uwaga: ta metoda jest nieco mniej zalecana od opcji 1, jak wspomniano wcześniej.

TfLiteDelegate* CreateDummyDelegateFromOptions(char** options_keys,
                                               char** options_values,
                                               size_t num_options) {
  DummyDelegateOptions options = TfLiteDummyDelegateOptionsDefault();

  // Parse key-values options to DummyDelegateOptions.
  // You can achieve this by mimicking them as command-line flags.
  std::unique_ptr<const char*> argv =
      std::unique_ptr<const char*>(new const char*[num_options + 1]);
  constexpr char kDummyDelegateParsing[] = "dummy_delegate_parsing";
  argv.get()[0] = kDummyDelegateParsing;

  std::vector<std::string> option_args;
  option_args.reserve(num_options);
  for (int i = 0; i < num_options; ++i) {
    option_args.emplace_back("--");
    option_args.rbegin()->append(options_keys[i]);
    option_args.rbegin()->push_back('=');
    option_args.rbegin()->append(options_values[i]);
    argv.get()[i + 1] = option_args.rbegin()->c_str();
  }

  // Define command-line flags.
  // ...
  std::vector<tflite::Flag> flag_list = {
      tflite::Flag::CreateFlag(...),
      ...,
      tflite::Flag::CreateFlag(...),
  };

  int argc = num_options + 1;
  if (!tflite::Flags::Parse(&argc, argv.get(), flag_list)) {
    return nullptr;
  }

  return TfLiteDummyDelegateCreate(&options);
}

#ifdef __cplusplus
extern "C" {
#endif  // __cplusplus

// Defines two symbols that need to be exported to use the TFLite external
// delegate. See tensorflow/lite/delegates/external for details.
TFL_CAPI_EXPORT TfLiteDelegate* tflite_plugin_create_delegate(
    char** options_keys, char** options_values, size_t num_options,
    void (*report_error)(const char*)) {
  return tflite::tools::CreateDummyDelegateFromOptions(
      options_keys, options_values, num_options);
}

TFL_CAPI_EXPORT void tflite_plugin_destroy_delegate(TfLiteDelegate* delegate) {
  TfLiteDummyDelegateDelete(delegate);
}

#ifdef __cplusplus
}
#endif  // __cplusplus

Utwórz teraz odpowiedni cel BUILD, aby utworzyć bibliotekę dynamiczną, jak pokazano poniżej:

cc_binary(
    name = "dummy_external_delegate.so",
    srcs = [
        "external_delegate_adaptor.cc",
    ],
    linkshared = 1,
    linkstatic = 1,
    deps = [
        ":dummy_delegate",
        "//tensorflow/lite/c:common",
        "//tensorflow/lite/tools:command_line_flags",
        "//tensorflow/lite/tools:logging",
    ],
)

Po utworzeniu pliku .so z delegatem zewnętrznym możesz tworzyć pliki binarne lub używać gotowych plików do uruchamiania z nowym delegatem, o ile plik binarny jest połączony z biblioteką external_delegate_provider, która obsługuje flagi wiersza poleceń, zgodnie z opisem tutaj. Uwaga: ten zewnętrzny dostawca przedstawicieli jest już połączony z istniejącymi plikami binarnymi testów i narzędzi.

Zajrzyj tutaj, aby dowiedzieć się, jak porównać przykładowego przedstawiciela w ramach takiego podejścia. Podobne polecenia możesz używać w przypadku wspomnianych wcześniej narzędzi do testowania i oceny.

Warto zauważyć, że przedstawiciel zewnętrzny to odpowiednie wdrożenie C++ delegata w powiązaniu Tensorflow Lite w Pythonie, jak pokazano tutaj. Dlatego utworzona tutaj dynamiczna biblioteka adaptera zewnętrznego delegata może być bezpośrednio używana z interfejsami API Tensorflow Lite w Pythonie.

Zasoby

System operacyjny ARCH BINARY_NAME
Linux x86_64
grupa eksperymentalna
aarch64
Android grupa eksperymentalna
aarch64