Dziękujemy za zapoznanie się z Google I/O. Zobacz wszystkie sesje na żądanie Oglądaj na żądanie

Implementacja niestandardowego delegata

Co to jest delegat TensorFlow Lite?

TensorFlow Lite Delegate umożliwia uruchamianie modeli (części lub całości) na innym executorze. Ten mechanizm może wykorzystywać do wnioskowania różne akceleratory na urządzeniu, takie jak GPU lub Edge TPU (jednostka przetwarzania tensorów). Zapewnia to programistom elastyczną i oddzieloną metodę od domyślnego TFLite, aby przyspieszyć wnioskowanie.

Poniższy diagram podsumowuje delegatów, więcej szczegółów w poniższych sekcjach.

TFLite Delegates

Kiedy należy utworzyć delegata niestandardowego?

TensorFlow Lite ma szeroką gamę delegatów dla akceleratorów docelowych, takich jak GPU, DSP, EdgeTPU i platform, takich jak Android NNAPI.

Tworzenie własnego pełnomocnika jest przydatne w następujących scenariuszach:

  • Chcesz zintegrować nowy aparat wnioskowania ML, który nie jest obsługiwany przez żadnego istniejącego delegata.
  • Masz niestandardowy akcelerator sprzętowy, który poprawia środowisko uruchomieniowe dla znanych scenariuszy.
  • Opracowujesz optymalizacje procesora (takie jak utrwalanie operatorów), które mogą przyspieszyć niektóre modele.

Jak działają delegaci?

Rozważ prosty wykres modelu, taki jak poniższy, oraz delegat „MyDelegate”, który ma szybszą implementację dla operacji Conv2D i Mean.

Original graph

Po zastosowaniu tego „MyDelegate” oryginalny wykres TensorFlow Lite zostanie zaktualizowany w następujący sposób:

Graph with delegate

Powyższy wykres jest uzyskiwany, gdy TensorFlow Lite dzieli oryginalny wykres według dwóch zasad:

  • Określone operacje, które mogą być obsługiwane przez delegata, są umieszczane na partycji, przy jednoczesnym zachowaniu oryginalnych zależności przepływu pracy obliczeniowej między operacjami.
  • Każda partycja, która ma zostać delegowana, ma tylko węzły wejściowe i wyjściowe, które nie są obsługiwane przez delegata.

Każda partycja obsługiwana przez delegata jest zastępowana przez węzeł delegata (może być również wywoływany jako jądro delegata) w oryginalnym grafie, który ocenia partycję podczas wywołania.

W zależności od modelu końcowy wykres może zawierać jeden lub więcej węzłów, co oznacza, że ​​niektóre operacje nie są obsługiwane przez delegata. Ogólnie rzecz biorąc, nie chcesz mieć wielu partycji obsługiwanych przez delegata, ponieważ za każdym razem, gdy przełączasz się z wykresu delegata na wykres główny, występuje obciążenie związane z przekazywaniem wyników z wykresu delegata do wykresu głównego, co wynika z pamięci kopie (na przykład z GPU na CPU). Takie obciążenie może zrównoważyć wzrost wydajności, zwłaszcza w przypadku dużej ilości kopii pamięci.

Wdrażanie własnego niestandardowego delegata

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

Aby utworzyć nowego delegata, musisz zaimplementować 2 interfejsy i podać własną implementację metod interfejsu.

1 — SimpleDelegateInterface

Ta klasa reprezentuje możliwości delegata, które operacje są obsługiwane, oraz klasę fabryczną do tworzenia jądra, które hermetyzuje delegowany wykres. Aby uzyskać więcej informacji, zobacz interfejs zdefiniowany w tym pliku nagłówkowym C++ . Komentarze w kodzie szczegółowo wyjaśniają każdy interfejs API.

2 — SimpleDelegateKernelInterface

Ta klasa hermetyzuje logikę inicjowania / przygotowywania / i uruchamiania delegowanej partycji.

Ma: (Patrz definicja )

  • Init(...): który zostanie wywołany raz w celu jednorazowej inicjalizacji.
  • Przygotuj(...): wywoływana dla każdej innej instancji tego węzła — dzieje się tak, jeśli masz wiele delegowanych partycji. Zwykle chcesz dokonać alokacji pamięci w tym miejscu, ponieważ będzie to wywoływane za każdym razem, gdy zmieniany jest rozmiar tensorów.
  • Invoke(...): który zostanie wywołany do wnioskowania.

Przykład

W tym przykładzie utworzysz bardzo prostego delegata, który może obsługiwać tylko 2 typy operacji (ADD) i (SUB) tylko 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, dziedzicząc po 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ównaj i oceń nowego delegata

TFLite posiada zestaw narzędzi, które możesz szybko przetestować z modelem TFLite.

  • Model Benchmark Tool : narzędzie pobiera model TFLite, generuje losowe dane wejściowe, a następnie wielokrotnie uruchamia model dla określonej liczby przebiegów. Na końcu drukuje zagregowane statystyki opóźnień.
  • Narzędzie różnicowania wnioskowania : dla danego modelu narzędzie generuje losowe dane Gaussa i przekazuje je przez dwa różne interpretery TFLite, jeden z jednowątkowym jądrem procesora, a drugi przy użyciu specyfikacji zdefiniowanej przez użytkownika. Mierzy bezwzględną różnicę między tensorami wyjściowymi z każdego interpretera na podstawie elementu. To narzędzie może być również pomocne przy debugowaniu problemów z dokładnością.
  • Istnieją również narzędzia oceny specyficzne dla zadań, służące do klasyfikacji obrazów i wykrywania obiektów. Te narzędzia można znaleźć tutaj

Ponadto TFLite ma duży zestaw testów jądra i jednostek operacyjnych, które można ponownie wykorzystać do przetestowania nowego delegata z większym pokryciem i zapewnienia, że ​​zwykła ścieżka wykonania TFLite nie zostanie zerwana.

Aby ponownie wykorzystać testy i narzędzia TFLite dla nowego delegata, możesz użyć jednej z dwóch następujących opcji:

Wybór najlepszego podejścia

Oba podejścia wymagają kilku zmian, które szczegółowo opisano poniżej. Jednak pierwsze podejście łączy delegata statycznie i wymaga przebudowy narzędzi testowania, analizy porównawczej i oceny. Natomiast drugi sprawia, że ​​delegat jest biblioteką współdzieloną i wymaga ujawnienia metod tworzenia/usuwania z biblioteki współdzielonej.

W rezultacie mechanizm delegowania zewnętrznego będzie działał z gotowymi plikami binarnymi narzędzi Tensorflow Lite TFLite . Jest to jednak mniej jednoznaczne i może być bardziej skomplikowane do skonfigurowania w automatycznych testach integracyjnych. Użyj podejścia rejestratora delegatów, aby uzyskać lepszą przejrzystość.

Opcja 1: Wykorzystanie rejestratora delegatów

Rejestrator delegatów prowadzi listę dostawców delegatów, z których każdy zapewnia łatwy sposób tworzenia delegatów TFLite w oparciu o flagi wiersza polecenia, a zatem jest wygodny w użyciu narzędzi. Aby podłączyć nowego delegata do wszystkich wymienionych powyżej narzędzi Tensorflow Lite, najpierw tworzysz nowego dostawcę delegatów, takiego jak ten , a następnie wprowadzasz tylko kilka zmian w regułach BUILD. Pełny przykład tego procesu integracji pokazano poniżej (a kod można znaleźć tutaj ).

Zakładając, że masz delegata, który implementuje interfejsy API SimpleDelegate oraz zewnętrzne interfejsy API „C” tworzenia/usuwania tego „fałszywego” delegata, 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ć „DummyDelegate” z Benchmark Tool i Inference Tool, zdefiniuj DelegateProvider, jak 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ż musisz upewnić się, że biblioteka jest zawsze połączona i nie jest usuwana przez optymalizator.

#### 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 dwie reguły opakowujące do pliku BUILD, aby utworzyć wersję narzędzia Benchmark Tool i Inference Tool oraz innych narzędzi ewaluacyjnych, które można uruchomić z własnym pełnomocnikiem.

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 również podłączyć tego delegowanego dostawcę do testów jądra TFLite, jak opisano tutaj .

Opcja 2: Wykorzystanie delegata zewnętrznego

W tej alternatywie należy najpierw utworzyć zewnętrzny adapter delegata, external_delegate_adaptor.cc , jak pokazano poniżej. Należy zauważyć, że to podejście jest nieco mniej preferowane w porównaniu z Opcją 1, jak już wspomniano .

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

Teraz utwórz odpowiedni cel BUILD, aby zbudować 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 tego zewnętrznego delegata pliku .so możesz zbudować pliki binarne lub użyć gotowych do uruchomienia z nowym delegatem, o ile plik binarny jest połączony z biblioteką external_delegate_provider , która obsługuje flagi wiersza poleceń, jak opisano tutaj . Uwaga: ten zewnętrzny dostawca delegowany został już połączony z istniejącymi plikami binarnymi testowania i narzędzi.

Zapoznaj się z opisami tutaj, aby zapoznać się z ilustracją, jak przeprowadzić test porównawczy fikcyjnego delegata za pomocą tego podejścia z zewnętrznym delegatem. Możesz użyć podobnych poleceń do narzędzi testowania i oceny wspomnianych wcześniej.

Warto zauważyć, że delegat zewnętrzny jest odpowiednią implementacją C++ delegata w wiązaniu Python Tensorflow Lite, jak pokazano tutaj . W związku z tym utworzona tutaj dynamiczna biblioteka adaptera delegatów zewnętrznych może być bezpośrednio używana z interfejsami API Tensorflow Lite Python.

Zasoby

OS ŁUK BINARY_NAME
Linux x86_64
ramię
arch64
Android ramię
arch64