Versions des opérateurs TensorFlow Lite

Ce document décrit le schéma de gestion des versions d'opérations de TensorFlow Lite. La gestion des versions des opérations permet aux développeurs d'ajouter de nouvelles fonctionnalités et de nouveaux paramètres aux opérations existantes. En outre, il garantit les éléments suivants:

  • Rétrocompatibilité: la nouvelle implémentation de TensorFlow Lite doit gérer un ancien fichier de modèle.
  • Compatibilité ascendante: l'ancienne implémentation de TensorFlow Lite doit gérer un nouveau fichier de modèle produit par la nouvelle version du convertisseur, à condition qu'aucune nouvelle fonctionnalité n'est utilisée.
  • Détection d'incompatibilité ascendante: si une ancienne implémentation de TensorFlow Lite lit un nouveau modèle contenant une nouvelle version d'une opération non compatible, l'erreur doit être signalée.

Exemple: Ajouter une dilation dans une convolution en profondeur

Le reste de ce document explique la gestion des versions d'opérations dans TFLite en montrant comment ajouter des paramètres de dilation à l'opération de convolution en profondeur.

Il n'est pas nécessaire de connaître la dilatation pour comprendre ce document. Remarques :

  • Deux paramètres entiers seront ajoutés: dilation_width_factor et dilation_height_factor.
  • Les anciens noyaux de convolution en profondeur qui ne sont pas compatibles avec la dilatation équivaut à définir les facteurs de dilation sur 1.

Modifier le schéma de FlatBuffer

Pour ajouter de nouveaux paramètres à une opération, modifiez la table d'options dans lite/schema/schema.fbs.

Par exemple, la table d'options de la convolution en profondeur se présente comme suit:

table DepthwiseConv2DOptions {
  padding:Padding;
  stride_w:int;
  stride_h:int;
  depth_multiplier:int;
  fused_activation_function:ActivationFunctionType;
}

Lorsque vous ajoutez des paramètres:

  • Ajoutez des commentaires indiquant les paramètres compatibles selon la version.
  • Lorsque la nouvelle implémentation obtient les valeurs par défaut des nouveaux paramètres ajoutés, elle devrait fonctionner exactement de la même manière que l'ancienne.

Une fois les nouveaux paramètres ajoutés, le tableau se présentera comme suit:

table DepthwiseConv2DOptions {
  // Parameters for DepthwiseConv version 1 or above.
  padding:Padding;
  stride_w:int;
  stride_h:int;
  depth_multiplier:int;
  fused_activation_function:ActivationFunctionType;
  // Parameters for DepthwiseConv version 2 or above.
  dilation_w_factor:int = 1;
  dilation_h_factor:int = 1;
}

Le fichier lite/schema/schema_generated.h doit être généré à nouveau pour le nouveau schéma.

Modifier les structures C et l'implémentation du noyau

Dans TensorFlow Lite, l'implémentation du noyau est découplée de la définition FlatBuffer. Les noyaux lisent le paramètre à partir des structures C définies dans lite/c/builtin_op_data.h.

Le paramètre initial de convolution en profondeur est le suivant:

typedef struct {
  TfLitePadding padding;
  int stride_width;
  int stride_height;
  int depth_multiplier;
  TfLiteFusedActivation activation;
} TfLiteDepthwiseConvParams;

Comme pour le schéma FlatBuffer, ajoutez des commentaires indiquant les paramètres compatibles en fonction de la version. Voici le résultat:

typedef struct {
  // Parameters for DepthwiseConv version 1 or above.
  TfLitePadding padding;
  int stride_width;
  int stride_height;
  int depth_multiplier;
  TfLiteFusedActivation activation;
  // Parameters for DepthwiseConv version 2 or above.
  int dilation_width_factor;
  int dilation_height_factor;
} TfLiteDepthwiseConvParams;

Veuillez également modifier l'implémentation du noyau pour lire les paramètres nouvellement ajoutés à partir des structures C. Les détails sont omis ici.

Modifier le code de lecture FlatBuffer

La logique pour lire FlatBuffer et produire une structure C se trouve dans lite/core/api/flatbuffer_conversions.cc.

Mettez à jour le fichier pour gérer les nouveaux paramètres, comme indiqué ci-dessous:

TfLiteStatus ParseDepthwiseConv2D(const Operator* op,
                                  ErrorReporter* error_reporter,
                                  BuiltinDataAllocator* allocator,
                                  void** builtin_data) {
  CheckParsePointerParams(op, error_reporter, allocator, builtin_data);

  SafeBuiltinDataAllocator safe_allocator(allocator);

  std::unique_ptr<TfLiteDepthwiseConvParams,
                  SafeBuiltinDataAllocator::BuiltinDataDeleter>
      params = safe_allocator.Allocate<TfLiteDepthwiseConvParams>();
  TF_LITE_ENSURE(error_reporter, params != nullptr);

  const DepthwiseConv2DOptions* schema_params =
      op->builtin_options_as_DepthwiseConv2DOptions();

  if (schema_params != nullptr) {
    params->padding = ConvertPadding(schema_params->padding());
    params->stride_width = schema_params->stride_w();
    params->stride_height = schema_params->stride_h();
    params->depth_multiplier = schema_params->depth_multiplier();
    params->activation =
        ConvertActivation(schema_params->fused_activation_function());

    params->dilation_width_factor = schema_params->dilation_w_factor();
    params->dilation_height_factor = schema_params->dilation_h_factor();
  }

  *builtin_data = params.release();
  return kTfLiteOk;
}

Il n'est pas nécessaire de vérifier la version de l'opération ici. Lorsque la nouvelle mise en œuvre lit un ancien fichier de modèle ne disposant pas de facteurs de dilatation, elle utilise 1 comme valeur par défaut, et le nouveau noyau fonctionne de manière cohérente avec l'ancien.

Modifier l'enregistrement du noyau

MutableOpResolver (défini dans lite/mutable_op_resolver.h) fournit quelques fonctions pour enregistrer les noyaux d'opérations. Par défaut, les versions minimale et maximale sont 1:

void AddBuiltin(tflite::BuiltinOperator op, TfLiteRegistration* registration,
                int min_version = 1, int max_version = 1);
void AddCustom(const char* name, TfLiteRegistration* registration,
               int min_version = 1, int max_version = 1);

Les opérations intégrées sont enregistrées dans lite/kernels/register.cc. Dans cet exemple, nous avons implémenté un nouveau noyau d'opération qui peut gérer les versions 1 et 2 de DepthwiseConv2D. Nous devons donc modifier cette ligne:

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D());

par :

AddBuiltin(BuiltinOperator_DEPTHWISE_CONV_2D, Register_DEPTHWISE_CONV_2D(),
             /* min_version = */ 1,
             /* max_version = */ 2);

Modifier la version de l'opération TFLite

L'étape suivante consiste à faire en sorte que TFLite renseigne la version minimale requise pour exécuter l'opération. Dans cet exemple, cela signifie:

  • Renseignez "version=1" lorsque les facteurs de dilation sont tous de 1.
  • Dans le cas contraire, renseignez version=2.

Modifiez la fonction GetBuiltinOperatorVersion pour l'opérateur dans lite/tools/versioning/op_version.cc en ajoutant la nouvelle version au cas de DepthwiseConv2D:

case BuiltinOperator_DEPTHWISE_CONV_2D:
  auto depthwise_conv_params =
      reinterpret_cast<TfLiteDepthwiseConvParams*>(op_sig.builtin_data);
  TFLITE_DCHECK(depthwise_conv_params != nullptr);
  if (depthwise_conv_params->dilation_width_factor != 1 ||
       depthwise_conv_params->dilation_height_factor != 1) {
    return 2;
  }
  return 1;

Mettre à jour le mappage des versions de l'opérateur

La dernière étape consiste à ajouter les informations de la nouvelle version au mappage de la version de l'opérateur. Cette étape est nécessaire, car nous devons générer la version d'exécution minimale requise pour le modèle en fonction de ce mappage de versions.

Pour ce faire, vous devez ajouter une nouvelle entrée de carte dans lite/tools/versioning/runtime_version.cc.

Dans cet exemple, vous devez ajouter l'entrée suivante dans op_version_map:

{ {BuiltinOperator_DEPTHWISE_CONV_2D, 2}, %CURRENT_RUNTIME_VERSION%}

%CURRENT_RUNTIME_VERSION% correspond à la version d'exécution actuelle définie dans tensorflow/core/public/version.h.

Implémentation de la délégation

TensorFlow Lite fournit une API de délégation qui permet de déléguer des opérations aux backends matériels. Dans la fonction Prepare du délégué, vérifiez si la version est compatible avec chaque nœud dans le code de délégation.

const int kMaxVersion = 1;
TfLiteNode* node;
TfLiteRegistration* registration = nullptr;
TF_LITE_ENSURE_STATUS(context->GetNodeAndRegistration(context, node_index, &node, &registration));

if (registration->version > kMaxVersion) {
  // Reject the node if the version isn't supported.
}

Cela est nécessaire même si la délégation n'est compatible qu'avec les opérations de version 1. Ainsi, la délégation peut détecter une incompatibilité lors de l'obtention d'une opération de version supérieure.