Wdrażanie niestandardowego przedstawiciela

LiteRT Opcja Przekazywanie dostępu umożliwia wykonywanie tych czynności: uruchamiać swoje modele (częściowo lub w całości) na innym wykonawcy. Mechanizm ten może wykorzystywać różnych akceleratorów działających na urządzeniu, takich jak GPU lub Edge TPU (Tensor) Processing Unit) do wnioskowania. Zapewnia to programistom elastyczność została odłączona od domyślnego TFLite, aby przyspieszyć wnioskowanie.

Schemat poniżej przedstawia podsumowanie przedstawicieli. Więcej informacji znajdziesz w sekcjach poniżej.

Delegaci TFLite

Kiedy należy utworzyć niestandardowego przedstawiciela?

LiteRT ma wielu przedstawicieli różnych akceleratorów docelowych, takich jak GPU, DSP i EdgeTPU.

Utworzenie własnego przedstawiciela jest przydatne w tych sytuacjach:

  • Chcesz zintegrować nowy mechanizm wnioskowania ML, który nie jest obsługiwany przez żadną z obecnego przedstawiciela.
  • Masz niestandardowy akcelerator sprzętowy, który poprawia czas działania znanych w różnych sytuacjach.
  • Opracowujesz optymalizacje procesora (takie jak fusing operatorów), które mogą i przyspieszają działanie określonych modeli.

Jak działają przedstawiciele?

Rozważ prosty wykres modelu, jak na przykład poniżej, i delegat „MyDelegate” który ma szybszą implementację obsługi Conv2D i średniej.

Oryginalny wykres

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

Wykres z delegatem

Powyższy wykres uzyskuje się, gdy oryginalny wykres jest dzielony przez LiteRT następujące dwie reguły:

  • Konkretne operacje, które może wykonywać przedstawiciel jest umieszczane w partycjonować, nie rezygnując przy tym z pierwotnego przepływu pracy zależności 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ą są obsługiwane przez przedstawiciela.

Każda partycja obsługiwana przez przedstawiciela jest zastępowana węzłem przedstawiciela (może być nazywane też jądrem delegującym) w oryginalnym wykresie, który ocenia partycji podczas jej wywołania.

W zależności od modelu ostateczny wykres może mieć jeden lub więcej węzłów, drugi, co oznacza, że dany przedstawiciel nie obsługuje niektórych operacji. Ogólnie rzecz biorąc, Nie chcesz, by osoba przekazująca dostęp obsługiwała wiele partycji, ponieważ każdy z nich gdy przechodzimy z wykresu delegata na główny, zwiększamy na przekazywanie do głównego wykresu wyników z delegowanego podgrafu z powodu kopii w pamięci (np. zamiany GPU na CPU). Takie narzuty mogą powodować kompensację wydajność rośnie, zwłaszcza gdy występuje duża liczba kopii w pamięci.

Wdrażanie własnego niestandardowego przedstawiciela

Preferowaną metodą dodawania przedstawiciela jest Interfejs SimpleDelegate API

Aby utworzyć nowego przedstawiciela, zaimplementuj 2 interfejsy i podaj swoje dane dla metod interfejsu.

1–SimpleDelegateInterface

Ta klasa reprezentuje możliwości przedstawiciela, czyli operacje, które są i klasę fabryczną do tworzenia jądra zawierającego wykres delegowany. Więcej informacji znajdziesz w interfejsie zdefiniowanym w tym Plik nagłówka C++. Komentarze w kodzie szczegółowo wyjaśniają poszczególne interfejsy API.

2–SimpleDelegateKernelInterface

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

Zawiera: (Zobacz definition)

  • Init(...): która jest wywoływana raz w celu przeprowadzenia jednorazowego zainicjowania.
  • Prepare(...): wywołany dla każdej instancji tego węzła - tak się dzieje jeśli masz wiele partycji z przekazanym dostępem. Zwykle chodzi o pamięć masową, alokacje w tym miejscu, ponieważ będzie ona wywoływana przy każdej zmianie rozmiaru tensorów.
  • Wywołaj(...): która zostanie wywołana do wnioskowania.

Przykład

W tym przykładzie utworzysz bardzo prostego przedstawiciela, który obsługuje tylko 2 osoby. tylko 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, dziedzicząc go 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_;
};

Przeprowadź analizę porównawczą i oceń nowych przedstawicieli

TFLite obejmuje zestaw narzędzi, które można szybko przetestować w modelu TFLite.

  • Narzędzie analizy porównawczej modeli: Narzędzie pobiera model TFLite, generuje losowe dane wejściowe, a potem wielokrotnie uruchamia model przez określoną liczbę uruchomień. Wyświetla zbiorcze opóźnienie statystyki na końcu.
  • Narzędzie różnicy wnioskowania: W przypadku danego modelu narzędzie generuje losowe dane Gaussa i przekazuje je za pomocą 2 różnych interpreterów TFLite, z których jeden działa na procesorach jednowątkowych w systemie Content ID. Mierzy wartość bezwzględną różnicy między tensorami wyjściowymi każdego interpretera na w odniesieniu do poszczególnych elementów. To narzędzie może też zwiększyć dokładność debugowania problemów.
  • Istnieją też narzędzia do oceny konkretnych zadań, na potrzeby klasyfikacji obrazów wykrywanie obiektów. Narzędzia te można znaleźć tutaj

TFLite ma też duży zestaw testów jądra i jednostek operacyjnych, które mogą być do przetestowania nowych przedstawicieli z szerszym zasięgiem i zagwarantowania, że standardowy Ścieżka wykonania TFLite nie jest uszkodzona.

Aby ponownie wykorzystać testy i narzędzia TFLite dla nowego przedstawiciela, możesz użyć opcji jedna z dwóch poniższych opcji:

Wybór najlepszego podejścia

Oba podejścia wymagają wprowadzenia kilku zmian, jak opisano poniżej. Jednak pierwszy statycznie łączy przedstawiciela i wymaga odbudowy testu, Testy porównawcze i oceny. Natomiast drugi sprawia, że jest przekazywana jako biblioteka udostępniona i wymaga ujawnienia instrukcji tworzenia/usuwania z zasobów wspólnych.

W efekcie mechanizm przekazywania dostępu poza domenę będzie działać z mechanizmami TFLite gotowych plików binarnych narzędzi LiteRT. Są one jednak mniej szczegółowe i mogą być trudniejsze do skonfigurowania w przypadku testy integracji. W celu uniknięcia wątpliwości użyj podejścia rejestratora z przekazanym dostępem.

Opcja 1. Korzystanie z przekazanego rejestratora

rejestrator z przekazanym dostępem prowadzi listę dostawców, z którymi każdy może łatwo określić, Informacje dla przedstawicieli TFLite oparte na flagach wiersza poleceń, są więc wygodne dla: narzędzi. Aby dodać nowego przedstawiciela do wszystkich wymienionych narzędzi LiteRT należy utworzyć nowego dostawcę przekazanego dostępu, a potem tylko kilka zmian w regułach BUILD. Pełny przykład proces integracji znajduje się poniżej (kod jest dostępny tutaj).

Zakładając, że masz przedstawiciela, który implementuje interfejsy SimpleDelegate API zewnętrzne „C” Interfejsy API tworzenia/usuwania tego fikcyjnego możesz przekazać dostęp w następujący sposób:

// 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 analizy porównawczej i inferencji, zdefiniuj 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 pomijana 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 2 reguły otoki do pliku BUILD, aby utworzyć wersję Analiza porównawcza i Narzędzie do wnioskowania oraz inne narzędzia do oceny, które można uruchomić z własnym przedstawicielem.

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ę delegacji do testów jądra TFLite zgodnie z opisem tutaj.

Opcja 2. Skorzystanie z przedstawiciela zewnętrznego

Najpierw należy utworzyć zewnętrzny adapter przedstawiciela external_delegate_adaptor.cc jak pokazano poniżej. Ta metoda jest nieco mniej preferowana w porównaniu z Opcja 1 zgodnie z opisem 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 element docelowy BUILD, aby utworzyć bibliotekę dynamiczną, jak pokazano na ilustracji 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 pliku .so możesz tworzyć pliki binarne lub używać gotowe do działania z nowym delegatem, dopóki plik binarny jest połączony z external_delegate_provider biblioteka, która obsługuje flagi wiersza poleceń zgodnie z opisem tutaj. Uwaga: ten zewnętrzny dostawca przekazywania dostępu został już połączony z istniejącym plików binarnych do testowania i konfigurowania plików binarnych.

Korzystanie z opisów tutaj na przykładzie tego testu porównawczego. zewnętrznej. Podobne polecenia możesz wykorzystać do testowania wspomnianych wcześniej narzędzi do oceny.

Pamiętaj, że przedstawiciel zewnętrzny to odpowiedni plik w języku C++ implementacja delegacji w powiązaniu Pythona LiteRT, jak pokazano na ilustracji tutaj. W związku z tym utworzona tutaj biblioteka dynamicznego adaptera zewnętrznego przedstawiciela może być bezpośrednio używane z interfejsami API LiteRT Python.

Zasoby

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