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

Comment fonctionnent les délégués ?

Considérez un modèle graphique simple tel que celui-ci 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 selon 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 du 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 graphique 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 souhaitez 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-graphe délégué au graphique principal qui résulte de la mémoire. copies (par exemple, GPU vers CPU). Une telle surcharge peut annuler les gains de performances, en particulier lorsqu'il existe un grand nombre 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é consiste à utiliser 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 d'usine 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 possède : (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 souhaitez effectuer 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 les 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_;
};


Benchmarker et évaluer le nouveau délégué

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

  • Outil de référence de modèle : l'outil prend un modèle TFLite, génère des entrées aléatoires, puis exécute le modèle à plusieurs reprises pour un nombre spécifié d'exécutions. Il imprime des statistiques de latence agrégées à 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 noyau de processeur monothread 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 à des tâches, pour la classification d'images et la détection d'objets. Ces outils peuvent être trouvés ici

De plus, TFLite dispose d'un large ensemble de tests de noyau et d'unités opérationnelles qui pourraient être réutilisés pour tester le nouveau délégué avec plus de couverture et pour garantir que le chemin d'exécution habituel de TFLite n'est pas interrompu.

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 changements comme détaillé 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, la seconde 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.

En conséquence, le mécanisme de délégué externe fonctionnera avec les binaires d'outils Tensorflow Lite prédéfinis de TFLite. Mais c’est moins explicite et cela pourrait être plus compliqué à mettre en place dans les tests d’intégration automatisés. Utilisez l’approche du registraire délégué pour une meilleure clarté.

Option 1 : Tirer parti du registraire des délégués

Le registraire de délégués conserve une liste de fournisseurs de délégués, chacun d'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 connecter le nouveau délégué à tous les outils Tensorflow Lite mentionnés ci-dessus, vous créez d'abord un nouveau fournisseur de délégué comme celui -ci , puis n'apportez 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 externes « C » pour créer/supprimer 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 l'outil de référence et l'outil d'inférence, 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 des 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 d'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 que 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 .so de délégué externe créé, vous pouvez créer des binaires ou utiliser des binaires prédéfinis pour les exécuter avec le nouveau délégué, à condition que le binaire soit lié à la bibliothèque external_delegate_provider qui prend en charge les indicateurs de ligne de commande comme décrit ici . Remarque : ce fournisseur de délégués externe a déjà été lié aux binaires de tests et d'outils existants.

Reportez-vous aux descriptions ici pour une illustration de la manière d'évaluer 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

Système d'exploitation CAMBRE BINARY_NAME
Linux x86_64
bras
aarch64
Android bras
aarch64