Implémenter un délégué personnalisé

Un littérature Déléguer vous permet de : exécuter vos modèles (en partie ou en totalité) sur un autre exécuteur. Ce mécanisme peut exploiter divers accélérateurs intégrés à l'appareil, tels que le GPU ou Edge TPU (Tensor (unité de traitement) pour l'inférence. Les développeurs bénéficient ainsi d'une dissociée de la couche TFLite par défaut pour accélérer l'inférence.

Le schéma ci-dessous résume les délégués. Vous trouverez plus de détails dans les sections ci-dessous.

Délégués TFLite

Quand créer un délégué personnalisé ?

LiteRT dispose d'un large éventail de délégués pour les accélérateurs cibles, GPU, DSP et EdgeTPU.

La création de votre propre délégué est utile dans les scénarios suivants:

  • Vous souhaitez intégrer un nouveau moteur d'inférence ML non compatible avec délégué existant.
  • Vous disposez d'un accélérateur matériel personnalisé qui améliore l'exécution des charges de travail connues différents scénarios.
  • Vous développez des optimisations de processeur (par exemple, la fusion d'opérateurs) accélèrent certains modèles.

Comment fonctionnent les délégués ?

Prenons l'exemple d'un graphique de modèle simple comme celui-ci, ainsi que d'un délégué "MyDelegate". qui offre une implémentation plus rapide des opérations Conv2D et moyennes.

Graphique d'origine

Après avoir appliqué ce "MyDelegate", le graphique LiteRT d'origine sera mis à jour comme suit:

Graphique avec délégué

Le graphique ci-dessus est obtenu lorsque LiteRT divise le graphique d'origine les deux règles suivantes:

  • Les opérations spécifiques qui peuvent être gérées par le délégué sont effectuées dans un tout en satisfaisant au workflow de calcul d'origine entre les opérations.
  • Chaque partition à déléguer ne dispose que de nœuds d'entrée et de sortie qui ne sont pas gérée par le délégué.

Chaque partition gérée par un délégué est remplacée par un nœud délégué (peut également appelé noyau délégué) dans le graphe original qui évalue lors de son appel d'appel.

Selon le modèle, le graphique final peut se retrouver avec un ou plusieurs nœuds, la seconde signifie que certaines opérations ne sont pas prises en charge par le délégué. En général, vous vous ne voulez pas que plusieurs partitions soient gérées par le délégué, car chaque lorsque vous passez du délégué au graphique principal, cela entraîne une surcharge en transmettant les résultats du sous-graphique délégué au graphique principal qui en résulte en raison de copies de mémoire (par exemple, GPU vers CPU). Ces frais généraux peuvent compenser des gains de performances, en particulier lorsqu'il y a beaucoup de copies de mémoire.

Implémenter votre propre délégué personnalisé

La méthode privilégiée pour ajouter un délégué est d'utiliser API SimpleDelegate :

Pour créer un délégué, vous devez implémenter deux interfaces et fournir votre propre implémentation pour les méthodes d'interface.

1 - SimpleDelegateInterface

Cette classe représente les capacités du délégué, c'est-à-dire les opérations et une classe Factory pour créer un noyau qui encapsule le graphique délégué. Pour en savoir plus, reportez-vous à l'interface définie dans ce Fichier d'en-tête C++. Les commentaires du code expliquent chaque API en détail.

2 – SimpleDelegateKernelInterface

Cette classe encapsule la logique d'initialisation, de préparation et d'exécution des partition déléguée.

Il présente les caractéristiques suivantes: (Voir définition)

  • Init(...): appelé une fois pour toute initialisation unique.
  • Prepare(...): appelé pour chaque instance différente de ce nœud. Cela se produit. si vous avez plusieurs partitions déléguées. En général, vous voulez faire de la mémoire des allocations ici, car il sera appelé à chaque fois que les Tensors sont redimensionnés.
  • Appeler(...): qui sera appelé pour l'inférence.

Exemple

Dans cet exemple, vous allez créer un délégué très simple qui n'accepte que deux types d'opérations (ADD) et (SUB) avec des Tensors de type float32 uniquement.

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

Ensuite, créez votre propre noyau délégué en héritant du 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_;
};

Effectuer une analyse comparative et évaluer le nouveau délégué

TFLite dispose d'un ensemble d'outils que vous pouvez tester rapidement sur un modèle TFLite.

  • Outil d'analyse comparative des modèles: L'outil prend un modèle TFLite, génère des entrées aléatoires, puis de manière répétée qui exécute le modèle pour un nombre déterminé d'exécutions. Elle imprime la latence agrégée. les statistiques à la fin.
  • Outil de différence d'inférence: Pour un modèle donné, l'outil génère des données gaussiennes aléatoires et les transmet via deux interpréteurs TFLite différents, l'un exécutant un processeur monothread et l'autre à l'aide d'une spécification définie par l'utilisateur. Elle mesure la valeur différence entre les Tensors de sortie de chaque interpréteur, sur une par élément. Cet outil peut aussi s'avérer utile pour le débogage les problèmes de sécurité.
  • Il existe également des outils d'évaluation spécifiques à une tâche, pour la classification d'images et la détection d'objets. Vous trouverez ces outils cliquez ici

De plus, TFLite dispose d'un vaste ensemble de tests unitaires et opérationnels du noyau réutilisé pour tester le nouveau délégué avec une meilleure couverture et pour s'assurer que le Le chemin d'exécution TFLite n'est pas défaillant.

Pour réutiliser les tests et outils TFLite pour le nouveau délégué, vous pouvez utiliser l'une des deux options suivantes:

Choisir la meilleure approche

Les deux approches nécessitent quelques modifications, comme indiqué ci-dessous. Toutefois, la première associe le délégué de manière statique et nécessite de recréer les tests, d'analyse comparative et d'évaluation. En revanche, le second délégué en tant que bibliothèque partagée et exige que vous expliquiez l'autorisation de la bibliothèque partagée.

Le mécanisme de délégation externe fonctionnera donc avec le service Binaires d'outils LiteRT prédéfinis. Elle est toutefois moins explicite et peut être plus compliquée à configurer dans des des tests d'intégration. Pour plus de clarté, utilisez l'approche du bureau d'enregistrement délégué.

Option 1: utiliser un bureau d'enregistrement délégué

La bureau d'enregistrement délégué conserve une liste de fournisseurs délégués, chacun d'entre eux permettant de créer facilement les délégués TFLite basés sur des indicateurs de ligne de commande et sont donc pratiques des outils. Pour connecter le nouveau délégué à tous les outils LiteRT mentionnés ci-dessus, vous créez d'abord un nouveau fournisseur délégué, puis n'apportez que quelques modifications aux règles CREATE. Un exemple complet de ceci processus d'intégration est présenté ci-dessous. Vous trouverez le code cliquez ici).

Supposons que vous disposiez d'un délégué qui implémente les API SimpleDelegate et que la fonction externe "C" API de création/suppression de ce "factice" délégué comme indiqué ci-dessous:

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

Pour intégrer "DummyDelegate" à l'outil d'analyse comparative et à l'outil d'inférence, définissez d'un DelegateProvider comme ci-dessous:

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*) {});
}

Les définitions des règles CREATE sont importantes, car vous devez vous assurer que est toujours associée et n'est pas supprimée par l'optimiseur.

#### 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.
)

Ajoutez maintenant ces deux règles de wrapper à votre fichier Build pour créer une version de les outils de benchmark, d'inférence et autres outils d'évaluation qui peuvent être exécutés avec votre propre délégué.

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",
    ],
)

Vous pouvez également brancher ce fournisseur délégué aux tests du noyau TFLite, comme décrit cliquez ici.

Option 2: Faire appel à un délégué externe

Dans cette alternative, vous allez d'abord créer un adaptateur de délégué externe external_delegate_adaptor.cc comme indiqué ci-dessous. Notez que cette approche est légèrement moins appréciée l'option 1 susmentionnée ;

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

Créez maintenant la cible Build correspondante pour créer une bibliothèque dynamique, comme indiqué ci-dessous. ci-dessous:

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",
    ],
)

Une fois ce fichier .so délégué externe créé, vous pouvez créer des binaires ou utiliser prédéfinis à exécuter avec le nouveau délégué, tant que le binaire est lié à la external_delegate_provider qui prend en charge les indicateurs de ligne de commande cliquez ici. Remarque: Ce fournisseur de délégation externe est déjà associé à un compte existant de tests et d'outils.

Voir les descriptions cliquez ici pour voir comment évaluer le délégué factice via ce délégué externe. Vous pouvez utiliser des commandes similaires pour les tests et les outils d'évaluation mentionnés précédemment.

Notez que le délégué externe est le C++ correspondant implémentation du délégué dans la liaison Python LiteRT, comme illustré cliquez ici. Par conséquent, la bibliothèque d'adaptateurs de délégués externes dynamiques créée ici pourrait être directement utilisé avec les API LiteRT Python.

Ressources

OS ARCH BINARY_NAME
Linux x86_64
groupe
aarch64
Android groupe
aarch64