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

Un délégué TensorFlow Lite vous permet d'exécuter vos modèles (en partie ou intégralement) 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 Processing Unit) pour l'inférence. Cela fournit aux développeurs une méthode flexible et dissociée de TFLite par défaut pour accélérer l'inférence.

Le schéma ci-dessous récapitule 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é ?

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

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

  • Vous souhaitez intégrer un nouveau moteur d'inférence ML qui n'est compatible avec aucun délégué existant.
  • Vous disposez d'un accélérateur matériel personnalisé qui améliore l'exécution pour des scénarios connus.
  • Vous développez des optimisations de processeur (telles que la fusion par opérateur) susceptibles d'accélérer certains modèles.

Comment fonctionnent les délégués ?

Prenons l'exemple d'un graphe de modèle simple, comme celui présenté ci-dessous, et d'un délégué "MyDele" dont l'implémentation est plus rapide pour les opérations Conv2D et Mean.

Graphique d'origine

Après l'application de cet élément "MyMetadata", le graphe TensorFlow Lite d'origine est mis à jour comme suit:

Graphique avec délégué

Le graphe ci-dessus est obtenu lorsque TensorFlow Lite divise le graphe d'origine selon deux règles:

  • Les opérations spécifiques pouvant être gérées par le délégué sont placées dans une partition tout en respectant les dépendances du workflow de calcul d'origine entre les opérations.
  • Chaque partition à déléguer ne contient 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é (également appelé noyau délégué) dans le graphique d'origine qui évalue la partition lors de son appel d'appel.

Selon le modèle, le graphe final peut se retrouver avec un ou plusieurs nœuds, ce qui signifie que certaines opérations ne sont pas prises en charge par le délégué. En général, il n'est pas souhaitable que plusieurs partitions soient gérées par le délégué, car chaque fois que vous passez du délégué au graphe principal, la transmission des résultats du sous-graphe délégué au graphe principal entraîne des frais supplémentaires en raison des copies de mémoire (par exemple, GPU vers processeur). Une telle surcharge peut compenser les gains de performances, en particulier lorsqu'il existe une grande quantité 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é consiste à utiliser l'API SimpleMetadata.

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é, les opérations compatibles et une classe de fabrique permettant de créer un noyau qui encapsule le graphe délégué. Pour en savoir plus, consultez l'interface définie dans ce fichier d'en-tête C++. Les commentaires du code décrivent chaque API en détail.

2 - SimpleDelegateKernelInterface

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

Il présente: (voir la 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. C'est ici que vous souhaitez généralement effectuer des allocations de mémoire, car cette méthode sera appelée chaque fois que les Tensors sont redimensionnés.
  • Appeler(...): pour appeler 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 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 par rapport à un modèle TFLite.

  • Outil de benchmark des modèles : l'outil utilise un modèle TFLite, génère des entrées aléatoires, puis exécute le modèle de façon répétée pendant un nombre d'exécutions spécifié. Il imprime des statistiques de latence agrégées à la fin.
  • Outil de comparaison d'inférence : 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 de processeur à thread unique et l'autre utilisant une spécification définie par l'utilisateur. Il mesure la différence absolue entre les Tensors de sortie de chaque interpréteur, 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 à certaines tâches, pour la classification d'images et la détection d'objets. Ces outils sont disponibles sur cette page.

En outre, TFLite dispose d'un vaste ensemble de tests unitaires de noyau et d'opérations qui peuvent être réutilisés pour tester le nouveau délégué avec une couverture plus étendue et pour s'assurer que le chemin d'exécution standard de TFLite n'est pas interrompu.

Pour 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 relie le délégué de manière statique et nécessite de recompiler les outils de test, d'analyse comparative et d'évaluation. En revanche, la seconde définit le délégué en tant que bibliothèque partagée et vous oblige à exposer les méthodes de création/suppression à partir de la bibliothèque partagée.

En conséquence, le mécanisme de délégation externe fonctionnera avec les binaires des outils TensorFlow Lite prédéfinis de TFLite. Toutefois, il est moins explicite et peut être plus compliqué à configurer dans les tests d'intégration automatisés. Pour plus de clarté, utilisez l'approche du bureau d'enregistrement délégué.

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

Le bureau d'enregistrement délégué conserve une liste de fournisseurs délégués, chacun offrant un moyen simple de créer des délégués TFLite à partir d'indicateurs de ligne de commande et s'avère donc pratique pour l'utilisation des outils. Pour connecter le nouveau délégué à tous les outils Tensorflow Lite mentionnés ci-dessus, vous devez d'abord créer un fournisseur délégué, puis apporter quelques modifications aux règles BUILD. Un exemple complet de ce processus d'intégration est présenté ci-dessous (et le code est disponible ici).

En supposant que vous ayez un délégué qui met en œuvre les API SimpleMetadata, et les API externes "C" permettant de 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 "DummyMetadata" à l'outil d'analyse comparative et à l'outil d'inférence, définissez un MetadataProvider 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 de compilation sont importantes, car vous devez vous assurer que la bibliothèque est toujours associé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 de wrapper à votre fichier BUILD pour créer une version de l'outil de benchmark et de l'outil d'inférence, 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: Faire appel à un délégué externe

Dans cette alternative, vous devez d'abord créer un adaptateur de délégué externe external_delegate_adaptor.cc, comme indiqué ci-dessous. Comme nous l'avons mentionné précédemment, cette approche est légèrement moins recommandée que l'option 1.

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 le fichier .so de délégué externe créé, vous pouvez compiler des binaires ou utiliser des binaires prédéfinis à exécuter avec le nouveau délégué, à condition qu'ils soient associés à la bibliothèque external_delegate_provider qui accepte les indicateurs de ligne de commande, comme décrit ici. Remarque: Ce fournisseur délégué externe a déjà été associé aux binaires de test et d'outils existants.

Reportez-vous aux descriptions ici pour savoir comment 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.

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

Ressources

OS ARCHE BINARY_NAME
Linux x86_64
groupe
aarch64
Android groupe
aarch64