Implementowanie delegata niestandardowego

Co to jest delegat TensorFlow Lite?

Delegat TensorFlow Lite umożliwia uruchamianie modeli (części lub całości) na innym executorze. Mechanizm ten może wykorzystywać do wnioskowania różne akceleratory znajdujące się na urządzeniu, takie jak procesor graficzny lub Edge TPU (jednostka przetwarzania tensorowego). Zapewnia to programistom elastyczną i oddzieloną od domyślnej metody TFLite metodę przyspieszenia wnioskowania.

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 frameworków, takich jak Android NNAPI.

Utworzenie własnego delegata 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 skraca czas wykonywania znanych scenariuszy.
  • Pracujesz nad optymalizacjami procesora (takimi jak łączenie operatorów), które mogą przyspieszyć niektóre modele.

Jak pracują delegaci?

Rozważmy prosty wykres modelu, taki jak poniższy, i delegata „MyDelegate”, który ma szybszą implementację 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 uzyskano, gdy TensorFlow Lite dzieli oryginalny wykres zgodnie z dwiema regułami:

  • Konkretne operacje, które mógłby obsłużyć delegat, są umieszczane w partycji, nadal spełniając oryginalne 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żna go również nazwać jądrem delegata) na oryginalnym wykresie, który ocenia partycję podczas jej wywołania.

W zależności od modelu końcowy wykres może zawierać jeden lub więcej węzłów, co oznacza, że ​​delegat nie obsługuje niektórych operacji. Ogólnie rzecz biorąc, nie chcesz, aby delegat obsługiwał wiele partycji, ponieważ za każdym razem, gdy przełączasz się z delegata na główny wykres, istnieje narzut związany z przekazywaniem wyników z delegowanego podgrafu do głównego wykresu, co wynika z pamięci kopie (na przykład GPU do CPU). Taki narzut może zrównoważyć wzrost wydajności, szczególnie w przypadku dużej ilości kopii pamięci.

Implementowanie własnego delegata niestandardowego

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

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

1 — SimpleDelegateInterface

Ta klasa reprezentuje możliwości delegata, obsługiwane operacje oraz klasę fabryczną do tworzenia jądra, które hermetyzuje delegowany wykres. Więcej szczegółów można znaleźć w interfejsie zdefiniowanym w tym pliku nagłówkowym C++ . Komentarze w kodzie szczegółowo wyjaśniają każdy interfejs API.

2 — SimpleDelegateKernelInterface

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

Posiada: (zobacz definicję )

  • Init(...): który zostanie wywołany raz w celu wykonania dowolnej jednorazowej inicjalizacji.
  • Przygotuj(...): wywoływane dla każdej innej instancji tego węzła - dzieje się tak, jeśli masz wiele delegowanych partycji. Zwykle chcesz tutaj dokonać alokacji pamięci, ponieważ będzie to wywoływane za każdym razem, gdy zmieniany jest rozmiar tensorów.
  • Invoke(...): który zostanie wywołany w celu 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 z 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ć w porównaniu z modelem TFLite.

  • Narzędzie do porównywania modeli : narzędzie pobiera model TFLite, generuje losowe dane wejściowe, a następnie wielokrotnie uruchamia model przez określoną liczbę przebiegów. Na końcu drukuje zagregowane statystyki opóźnień.
  • Narzędzie porównywania wniosków : dla danego modelu narzędzie generuje losowe dane Gaussa i przepuszcza je przez dwa różne interpretery TFLite, jeden obsługujący jednowątkowe jądro procesora, a drugi wykorzystujący specyfikację zdefiniowaną przez użytkownika. Mierzy bezwzględną różnicę między tensorami wyjściowymi każdego interpretera w przeliczeniu na element. 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 zadania, do klasyfikacji obrazów i wykrywania obiektów. Narzędzia te można znaleźć tutaj

Ponadto TFLite posiada 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 upewnienia się, że zwykła ścieżka wykonywania TFLite nie zostanie uszkodzona.

Aby ponownie wykorzystać testy i narzędzia TFLite dla nowego delegata, możesz skorzystać z jednej z dwóch poniższych opcji:

Wybór najlepszego podejścia

Obydwa podejścia wymagają kilku zmian, jak opisano szczegółowo poniżej. Jednak pierwsze podejście łączy delegata statycznie i wymaga przebudowania narzędzi do testowania, porównywania i oceny. Natomiast drugi sprawia, że ​​delegat staje się biblioteką współdzieloną i wymaga udostępnienia metod tworzenia/usuwania z biblioteki współdzielonej.

W rezultacie mechanizm delegowania zewnętrznego będzie współpracował z gotowymi plikami binarnymi narzędzi Tensorflow Lite TFLite. Jest to jednak mniej jednoznaczne i konfiguracja w zautomatyzowanych testach integracyjnych może być bardziej skomplikowana. Aby uzyskać większą przejrzystość, użyj podejścia rejestratora delegowanego.

Opcja 1: Rejestrator delegujący dźwignię

Rejestrator delegatów przechowuje listę dostawców delegatów, z których każdy zapewnia łatwy sposób tworzenia delegatów TFLite w oparciu o flagi wiersza poleceń, dzięki czemu jest wygodny w użyciu. Aby podłączyć nowego delegata do wszystkich wspomnianych powyżej narzędzi Tensorflow Lite, najpierw utwórz nowego dostawcę delegata, takiego jak ten , a następnie dokonaj tylko kilku 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 i zewnętrzne interfejsy API „C” służące do tworzenia/usuwania tego „fikcyjnego” 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 narzędziem Benchmark Tool i narzędziem wnioskowania, 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ż należy 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 opakowania do pliku BUILD, aby utworzyć wersję narzędzia Benchmark Tool i narzędzia wnioskowania oraz innych narzędzi ewaluacyjnych, które można uruchomić z własnym delegatem.

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

Opcja 2: Wykorzystaj zewnętrznego delegata

W tej alternatywie najpierw tworzysz zewnętrzny adapter delegata o nazwie 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 wspomniano powyżej .

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 pliku .so zewnętrznego delegata można budować pliki binarne lub używać gotowych plików binarnych do uruchamiania 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 delegatów został już połączony z istniejącymi plikami binarnymi testowania i narzędzi.

Zapoznaj się z opisami tutaj , aby zapoznać się z ilustracją sposobu porównywania fikcyjnego delegata za pomocą podejścia z udziałem zewnętrznego delegata. Podobnych poleceń można używać w przypadku wspomnianych wcześniej narzędzi do testowania i oceny.

Warto zauważyć , że delegat zewnętrzny jest odpowiednią implementacją delegata w języku C++ w powiązaniu Tensorflow Lite Python, jak pokazano tutaj . Dlatego utworzonej tutaj dynamicznej biblioteki adapterów delegatów zewnętrznych można bezpośrednio używać z interfejsami API Tensorflow Lite Python.

Zasoby

system operacyjny ŁUK NAZWA BINARY
Linuksa x86_64
ramię
aarch64
Android ramię
aarch64