Implémentation d'un délégué personnalisé

Qu'est-ce qu'un délégué TensorFlow Lite ?

Un délégué TensorFlow Lite vous permet d'exécuter vos modèles (en partie ou en totalité) sur un autre exécuteur. Ce mécanisme peut tirer parti d'une variété d'accélérateurs sur l'appareil tels que le GPU ou Edge TPU (Tensor Processing Unit) pour l'inférence. Cela fournit aux développeurs une méthode flexible et découplée du TFLite par défaut pour accélérer l'inférence.

Le diagramme ci-dessous résume les délégués, plus de détails dans les sections ci-dessous.

TFLite Delegates

Quand dois-je créer un délégué personnalisé ?

TensorFlow Lite dispose d'une grande variété de délégués pour les accélérateurs cibles tels que GPU, DSP, EdgeTPU et des frameworks comme Android NNAPI.

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 qui n'est pris en charge par aucun délégué existant.
  • Vous disposez d'un accélérateur matériel personnalisé qui améliore l'exécution des scénarios connus.
  • Vous développez des optimisations CPU (telles que la fusion d'opérateurs) qui peuvent accélérer certains modèles.

Comment fonctionnent les délégués ?

Considérez un graphe de modèle simple tel que le suivant et un délégué "MyDelegate" qui a une implémentation plus rapide pour les opérations Conv2D et Mean.

Original graph

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

Graph with delegate

Le graphique ci-dessus est obtenu lorsque TensorFlow Lite divise le graphique d'origine en suivant deux règles :

  • Les opérations spécifiques qui pourraient être gérées par le délégué sont placées dans une partition tout en satisfaisant les dépendances de flux de travail informatique d'origine entre les opérations.
  • Chaque partition à déléguer n'a que des nœuds d'entrée et de sortie qui ne sont pas gérés 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 être appelé en tant que noyau délégué) dans le graphe d'origine qui évalue la partition lors de son appel d'invocation.

Selon le modèle, le graphe final peut se retrouver avec un ou plusieurs nœuds, ce dernier signifiant que certaines opérations ne sont pas prises en charge par le délégué. En général, vous ne voulez pas que plusieurs partitions soient gérées par le délégué, car chaque fois que vous passez du délégué au graphique principal, il y a une surcharge pour transmettre les résultats du sous-graphique délégué au graphique principal qui résulte en raison de la mémoire copies (par exemple, GPU vers CPU). Une telle surcharge peut compenser les gains de performances, en particulier lorsqu'il existe une grande quantité de copies de mémoire.

Implémentation de votre propre délégué personnalisé

La méthode préférée pour ajouter un délégué utilise l' API SimpleDelegate .

Pour créer un nouveau délégué, vous devez implémenter 2 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é, les opérations prises en charge et une classe de fabrique pour créer un noyau qui encapsule le graphe délégué. Pour plus de détails, consultez l'interface définie dans ce fichier d'en-tête C++ . Les commentaires dans le code expliquent chaque API en détail.

2 - SimpleDelegateKernelInterface

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

Il a : (Voir définition )

  • Init(...) : qui sera appelé une fois pour effectuer une 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. Habituellement, vous voulez faire des allocations de mémoire ici, car cela sera appelé à chaque fois que les tenseurs sont redimensionnés.
  • Invoke(...) : qui sera appelé pour l'inférence.

Exemple

Dans cet exemple, vous allez créer un délégué très simple qui ne peut prendre en charge que 2 types d'opérations (ADD) et (SUB) avec des tenseurs 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 de 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_;
};


Benchmark et évaluation du nouveau délégué

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

  • Model Benchmark Tool : L'outil prend un modèle TFLite, génère des entrées aléatoires, puis exécute à plusieurs reprises le modèle pour un nombre spécifié d'exécutions. Il imprime des statistiques de latence agrégées à la fin.
  • Inference Diff Tool : Pour un modèle donné, l'outil génère des données gaussiennes aléatoires et les transmet à deux interpréteurs TFLite différents, l'un exécutant un noyau CPU à thread unique et l'autre utilisant une spécification définie par l'utilisateur. Il mesure la différence absolue entre les tenseurs de sortie de chaque interpréteur, élément par élément. Cet outil peut également être utile pour déboguer les problèmes de précision.
  • Il existe également des outils d'évaluation spécifiques à une tâche, pour la classification d'images et la détection d'objets. Ces outils sont disponibles ici

De plus, TFLite dispose d'un large ensemble de tests unitaires de noyau et d'opération qui pourraient être réutilisés pour tester le nouveau délégué avec plus de couverture et pour s'assurer que le chemin d'exécution TFLite normal n'est pas rompu.

Pour parvenir à réutiliser les tests et les 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. Cependant, la première approche lie le délégué de manière statique et nécessite de reconstruire les outils de test, de benchmarking et d'évaluation. En revanche, le second fait du délégué une bibliothèque partagée et vous oblige à exposer les méthodes de création/suppression de la bibliothèque partagée.

Par conséquent, le mécanisme de délégué externe fonctionnera avec les binaires d' outils Tensorflow Lite pré-construits de TFLite . Mais c'est moins explicite et c'est peut-être plus compliqué à mettre en place dans les tests d'intégration automatisés. Utilisez l'approche du bureau d'enregistrement délégué pour plus de clarté.

Option 1 : tirer parti du bureau d'enregistrement délégué

Le bureau d'enregistrement des délégués conserve une liste de fournisseurs de délégués, chacun d'entre eux fournissant un moyen simple de créer des délégués TFLite basés sur des indicateurs de ligne de commande, et sont donc pratiques pour l'outillage. Pour brancher le nouveau délégué à tous les outils Tensorflow Lite mentionnés ci-dessus, vous devez d'abord créer un nouveau fournisseur délégué comme celui- ci , puis n'apporter que quelques modifications aux règles BUILD. Un exemple complet de ce processus d'intégration est présenté ci-dessous (et le code peut être trouvé ici ).

En supposant que vous ayez un délégué qui implémente les API SimpleDelegate et les API "C" externes de création/suppression de ce délégué "factice", 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 le "DummyDelegate" avec Benchmark Tool et Inference Tool, définissez 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 de règles BUILD sont importantes car vous devez vous assurer que la bibliothèque est toujours liée et non 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 wrapper dans votre fichier BUILD pour créer une version de Benchmark Tool et Inference Tool, ainsi que d'autres outils d'évaluation, qui pourraient s'exécuter 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 connecter ce fournisseur délégué aux tests du noyau TFLite comme décrit ici .

Option 2 : tirer parti d'un délégué externe

Dans cette alternative, vous créez d'abord un adaptateur de délégué externe external_delegate_adaptor.cc comme indiqué ci-dessous. Notez que cette approche est légèrement moins préférée par rapport à l'option 1, comme cela a été mentionné ci -dessus.

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 :

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 délégué externe .so créé, vous pouvez créer des fichiers binaires ou en utiliser des pré-construits pour les exécuter avec le nouveau délégué tant que le fichier binaire est lié à la bibliothèque external_delegate_provider qui prend en charge les indicateurs de ligne de commande comme décrit ici . Remarque : ce fournisseur délégué externe a déjà été lié à des binaires de test et d'outils existants.

Reportez-vous aux descriptions ici pour une illustration de la façon de comparer le délégué factice via cette approche de délégué externe. Vous pouvez utiliser des commandes similaires pour les outils de test et d'évaluation mentionnés précédemment.

Il convient de noter que le délégué externe est l'implémentation C++ correspondante du délégué dans la liaison Tensorflow Lite Python, comme indiqué ici . Par conséquent, la bibliothèque d'adaptateurs de délégués externes dynamiques créée ici pourrait être directement utilisée avec les API Python Tensorflow Lite.

Ressources

SE CAMBRE BINARY_NAME
Linux x86_64
bras
aarch64
Android bras
aarch64